telegix 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +615 -14
- package/index.cjs +6657 -3272
- package/index.js +66 -2
- package/lib/api.js +270 -18
- package/lib/context.js +201 -2
- package/lib/ephemeral.js +353 -0
- package/lib/rich.js +1992 -159
- package/lib/serialize.js +11 -0
- package/lib/table.js +1124 -0
- package/package.json +1 -1
package/lib/table.js
ADDED
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegix - Table Generator & Formatting Engine
|
|
3
|
+
* Generates beautiful formatted ASCII / Unicode box tables for Telegram HTML (<pre>)
|
|
4
|
+
* and structured InputRichBlockTable / RichBlockTable for Telegram Bot API 10.3+.
|
|
5
|
+
* @module telegix/table
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { escapeHtml } from './format.js';
|
|
9
|
+
|
|
10
|
+
// Table Border Box Styles
|
|
11
|
+
const STYLES = {
|
|
12
|
+
box: {
|
|
13
|
+
topLeft: '┌',
|
|
14
|
+
topMid: '┬',
|
|
15
|
+
topRight: '┐',
|
|
16
|
+
midLeft: '├',
|
|
17
|
+
midMid: '┼',
|
|
18
|
+
midRight: '┤',
|
|
19
|
+
bottomLeft: '└',
|
|
20
|
+
bottomMid: '┴',
|
|
21
|
+
bottomRight: '┘',
|
|
22
|
+
horizontal: '─',
|
|
23
|
+
vertical: '│',
|
|
24
|
+
},
|
|
25
|
+
ascii: {
|
|
26
|
+
topLeft: '+',
|
|
27
|
+
topMid: '+',
|
|
28
|
+
topRight: '+',
|
|
29
|
+
midLeft: '+',
|
|
30
|
+
midMid: '+',
|
|
31
|
+
midRight: '+',
|
|
32
|
+
bottomLeft: '+',
|
|
33
|
+
bottomMid: '+',
|
|
34
|
+
bottomRight: '+',
|
|
35
|
+
horizontal: '-',
|
|
36
|
+
vertical: '|',
|
|
37
|
+
},
|
|
38
|
+
compact: {
|
|
39
|
+
topLeft: '',
|
|
40
|
+
topMid: ' ',
|
|
41
|
+
topRight: '',
|
|
42
|
+
midLeft: '',
|
|
43
|
+
midMid: '┼',
|
|
44
|
+
midRight: '',
|
|
45
|
+
bottomLeft: '',
|
|
46
|
+
bottomMid: '',
|
|
47
|
+
bottomRight: '',
|
|
48
|
+
horizontal: '─',
|
|
49
|
+
vertical: '│',
|
|
50
|
+
},
|
|
51
|
+
clean: {
|
|
52
|
+
topLeft: '',
|
|
53
|
+
topMid: '',
|
|
54
|
+
topRight: '',
|
|
55
|
+
midLeft: '',
|
|
56
|
+
midMid: ' ',
|
|
57
|
+
midRight: '',
|
|
58
|
+
bottomLeft: '',
|
|
59
|
+
bottomMid: '',
|
|
60
|
+
bottomRight: '',
|
|
61
|
+
horizontal: '─',
|
|
62
|
+
vertical: ' ',
|
|
63
|
+
},
|
|
64
|
+
card: {
|
|
65
|
+
topLeft: '╭',
|
|
66
|
+
topMid: '┬',
|
|
67
|
+
topRight: '╮',
|
|
68
|
+
midLeft: '├',
|
|
69
|
+
midMid: '┼',
|
|
70
|
+
midRight: '┤',
|
|
71
|
+
bottomLeft: '╰',
|
|
72
|
+
bottomMid: '┴',
|
|
73
|
+
bottomRight: '╯',
|
|
74
|
+
horizontal: '─',
|
|
75
|
+
vertical: '│',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Calculate visual string length (supports basic emojis and numbers)
|
|
81
|
+
* @param {any} val
|
|
82
|
+
* @returns {number}
|
|
83
|
+
*/
|
|
84
|
+
function visualLength(val) {
|
|
85
|
+
if (val === null || val === undefined) return 0;
|
|
86
|
+
return String(val).length;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Pad a string to target width with alignment
|
|
91
|
+
* @param {any} val
|
|
92
|
+
* @param {number} width
|
|
93
|
+
* @param {'left'|'center'|'right'} [align='left']
|
|
94
|
+
* @returns {string}
|
|
95
|
+
*/
|
|
96
|
+
function pad(val, width, align = 'left') {
|
|
97
|
+
const str = val === null || val === undefined ? '' : String(val);
|
|
98
|
+
const diff = width - visualLength(str);
|
|
99
|
+
if (diff <= 0) return str;
|
|
100
|
+
|
|
101
|
+
if (align === 'right') {
|
|
102
|
+
return ' '.repeat(diff) + str;
|
|
103
|
+
}
|
|
104
|
+
if (align === 'center') {
|
|
105
|
+
const left = Math.floor(diff / 2);
|
|
106
|
+
const right = diff - left;
|
|
107
|
+
return ' '.repeat(left) + str + ' '.repeat(right);
|
|
108
|
+
}
|
|
109
|
+
return str + ' '.repeat(diff);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Escape string for SVG XML
|
|
114
|
+
* @param {any} val
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
function escapeSvg(val) {
|
|
118
|
+
if (val === null || val === undefined) return '';
|
|
119
|
+
return String(val)
|
|
120
|
+
.replace(/&/g, '&')
|
|
121
|
+
.replace(/</g, '<')
|
|
122
|
+
.replace(/>/g, '>')
|
|
123
|
+
.replace(/"/g, '"')
|
|
124
|
+
.replace(/'/g, ''');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Table builder class for Telegram messages and Rich Blocks
|
|
129
|
+
*/
|
|
130
|
+
export class Table {
|
|
131
|
+
/**
|
|
132
|
+
* @param {Array<string>|object} [headersOrOptions={}]
|
|
133
|
+
* @param {Array<Array<any>>} [rows=[]]
|
|
134
|
+
* @param {object} [options={}]
|
|
135
|
+
*/
|
|
136
|
+
constructor(headersOrOptions = {}, rows = [], options = {}) {
|
|
137
|
+
this.type = 'table';
|
|
138
|
+
let opts = {};
|
|
139
|
+
if (Array.isArray(headersOrOptions)) {
|
|
140
|
+
this.headers = [...headersOrOptions];
|
|
141
|
+
this.rows = Array.isArray(rows) ? rows.map((r) => [...r]) : [];
|
|
142
|
+
opts = options || {};
|
|
143
|
+
} else {
|
|
144
|
+
opts = headersOrOptions || {};
|
|
145
|
+
this.headers = opts.headers ? [...opts.headers] : [];
|
|
146
|
+
this.rows = opts.rows ? opts.rows.map((r) => [...r]) : [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.is_bordered = Boolean(opts.is_bordered ?? opts.isBordered ?? true);
|
|
150
|
+
this.is_compact = Boolean(opts.is_compact ?? opts.isCompact ?? false);
|
|
151
|
+
this.is_striped = Boolean(opts.is_striped ?? opts.isStriped ?? false);
|
|
152
|
+
this.caption = opts.caption || '';
|
|
153
|
+
this._style = opts.style || 'card';
|
|
154
|
+
this._title = opts.title || '';
|
|
155
|
+
this._alignments = opts.alignments ? [...opts.alignments] : [];
|
|
156
|
+
this.col1Width = opts.col1Width;
|
|
157
|
+
this.asImage = Boolean(opts.asImage || opts.photo || opts.image);
|
|
158
|
+
this._cardOptions = { ...opts };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Set headers
|
|
163
|
+
* @param {Array<string>|...string} headers
|
|
164
|
+
* @returns {this}
|
|
165
|
+
*/
|
|
166
|
+
header(...headers) {
|
|
167
|
+
if (headers.length === 1 && Array.isArray(headers[0])) {
|
|
168
|
+
this.headers = [...headers[0]];
|
|
169
|
+
} else {
|
|
170
|
+
this.headers = headers.flat();
|
|
171
|
+
}
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Set headers (alias)
|
|
177
|
+
* @param {Array<string>} headers
|
|
178
|
+
* @returns {this}
|
|
179
|
+
*/
|
|
180
|
+
setHeaders(headers) {
|
|
181
|
+
this.headers = Array.isArray(headers) ? [...headers] : [];
|
|
182
|
+
return this;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Add a row
|
|
187
|
+
* @param {Array<any>|...any} cells
|
|
188
|
+
* @returns {this}
|
|
189
|
+
*/
|
|
190
|
+
row(...cells) {
|
|
191
|
+
if (cells.length === 1 && Array.isArray(cells[0])) {
|
|
192
|
+
this.rows.push([...cells[0]]);
|
|
193
|
+
} else {
|
|
194
|
+
this.rows.push(cells.flat());
|
|
195
|
+
}
|
|
196
|
+
return this;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Add multiple rows
|
|
201
|
+
* @param {Array<Array<any>>} rows
|
|
202
|
+
* @returns {this}
|
|
203
|
+
*/
|
|
204
|
+
addRows(rows) {
|
|
205
|
+
if (Array.isArray(rows)) {
|
|
206
|
+
for (const r of rows) {
|
|
207
|
+
this.row(r);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return this;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Set table compact mode (Telegram Bot API 10.3)
|
|
215
|
+
* @param {boolean} [isCompact=true]
|
|
216
|
+
* @returns {this}
|
|
217
|
+
*/
|
|
218
|
+
compact(isCompact = true) {
|
|
219
|
+
this.is_compact = Boolean(isCompact);
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Set visual style ('box' | 'ascii' | 'compact' | 'clean' | 'markdown')
|
|
225
|
+
* @param {string} styleName
|
|
226
|
+
* @returns {this}
|
|
227
|
+
*/
|
|
228
|
+
style(styleName) {
|
|
229
|
+
this._style = styleName;
|
|
230
|
+
return this;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Set optional table title
|
|
235
|
+
* @param {string} title
|
|
236
|
+
* @returns {this}
|
|
237
|
+
*/
|
|
238
|
+
title(title) {
|
|
239
|
+
this._title = title;
|
|
240
|
+
return this;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Set column alignment
|
|
245
|
+
* @param {number} colIndex
|
|
246
|
+
* @param {'left'|'center'|'right'} align
|
|
247
|
+
* @returns {this}
|
|
248
|
+
*/
|
|
249
|
+
columnAlign(colIndex, align) {
|
|
250
|
+
this._alignments[colIndex] = align;
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Set alignments for all columns
|
|
256
|
+
* @param {Array<'left'|'center'|'right'>} aligns
|
|
257
|
+
* @returns {this}
|
|
258
|
+
*/
|
|
259
|
+
alignments(aligns) {
|
|
260
|
+
this._alignments = Array.isArray(aligns) ? [...aligns] : [];
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Render table as plain formatted monospaced text
|
|
266
|
+
* @param {object} [options]
|
|
267
|
+
* @returns {string}
|
|
268
|
+
*/
|
|
269
|
+
format(options = {}) {
|
|
270
|
+
const opts = typeof options === 'string' ? { style: options } : (options || {});
|
|
271
|
+
return Table.format(this.headers, this.rows, {
|
|
272
|
+
style: opts.style || this._style,
|
|
273
|
+
isCompact: opts.is_compact ?? opts.isCompact ?? this.is_compact,
|
|
274
|
+
alignments: opts.alignments || this._alignments,
|
|
275
|
+
title: opts.title || this._title,
|
|
276
|
+
...opts,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* String coercion
|
|
282
|
+
*/
|
|
283
|
+
toString() {
|
|
284
|
+
return this.format();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Render table as HTML wrapped in <pre> tags for Telegram
|
|
289
|
+
* @param {object} [options]
|
|
290
|
+
* @returns {string}
|
|
291
|
+
*/
|
|
292
|
+
toHtml(options = {}) {
|
|
293
|
+
const formatted = this.format(options);
|
|
294
|
+
const title = options.title || this._title;
|
|
295
|
+
const titleHtml = title ? `<b>${escapeHtml(title)}</b>\n\n` : '';
|
|
296
|
+
return `${titleHtml}<pre>${escapeHtml(formatted)}</pre>`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Render table as Markdown (or inside ``` code block)
|
|
301
|
+
* @param {object} [options]
|
|
302
|
+
* @returns {string}
|
|
303
|
+
*/
|
|
304
|
+
toMarkdown(options = {}) {
|
|
305
|
+
return Table.markdown(this.headers, this.rows, options);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Set table bordered mode (Telegram Bot API 10.3 / Card style)
|
|
310
|
+
* @param {boolean} [isBordered=true]
|
|
311
|
+
* @returns {this}
|
|
312
|
+
*/
|
|
313
|
+
bordered(isBordered = true) {
|
|
314
|
+
this.is_bordered = Boolean(isBordered);
|
|
315
|
+
return this;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Set table striped mode (Telegram Bot API 10.3)
|
|
320
|
+
* @param {boolean} [isStriped=true]
|
|
321
|
+
* @returns {this}
|
|
322
|
+
*/
|
|
323
|
+
striped(isStriped = true) {
|
|
324
|
+
this.is_striped = Boolean(isStriped);
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Convert table data into 2D array of RichBlockTableCell for Telegram Bot API 10.3
|
|
330
|
+
* @returns {Array<Array<object>>}
|
|
331
|
+
*/
|
|
332
|
+
toCells() {
|
|
333
|
+
const cells = [];
|
|
334
|
+
if (this.headers.length > 0) {
|
|
335
|
+
cells.push(
|
|
336
|
+
this.headers.map((h, i) => ({
|
|
337
|
+
text: String(h ?? ''),
|
|
338
|
+
is_header: true,
|
|
339
|
+
align: this._alignments[i] || 'center',
|
|
340
|
+
valign: 'middle',
|
|
341
|
+
}))
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
for (const row of this.rows) {
|
|
345
|
+
cells.push(
|
|
346
|
+
row.map((cell, i) => {
|
|
347
|
+
if (cell && typeof cell === 'object' && cell.text !== undefined) {
|
|
348
|
+
return {
|
|
349
|
+
align: this._alignments[i] || 'left',
|
|
350
|
+
valign: 'middle',
|
|
351
|
+
...cell,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
text: String(cell ?? ''),
|
|
356
|
+
align: this._alignments[i] || 'left',
|
|
357
|
+
valign: 'middle',
|
|
358
|
+
};
|
|
359
|
+
})
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return cells;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Render table as SVG vector graphic matching the Telegram Bot Card Table UI
|
|
367
|
+
* (dark rounded container, grid borders, headers, and blue clickable links)
|
|
368
|
+
* @param {object} [options]
|
|
369
|
+
* @returns {string} SVG XML markup string
|
|
370
|
+
*/
|
|
371
|
+
toCardSvg(options = {}) {
|
|
372
|
+
const width = Number(options.width || 420);
|
|
373
|
+
const maxLabelLen = Math.max(
|
|
374
|
+
...this.rows.map((r) => String(r[0] ?? '').length),
|
|
375
|
+
this.headers[0] ? String(this.headers[0]).length : 0,
|
|
376
|
+
10
|
|
377
|
+
);
|
|
378
|
+
const calculatedCol1 = Math.max(130, Math.min(220, maxLabelLen * 9 + 30));
|
|
379
|
+
const col1Width = Number(options.col1Width || this.col1Width || calculatedCol1);
|
|
380
|
+
const headerHeight = Number(options.headerHeight || 38);
|
|
381
|
+
const rowHeight = Number(options.rowHeight || 36);
|
|
382
|
+
const hasHeader = this.headers.length > 0;
|
|
383
|
+
const headerH = hasHeader ? headerHeight : 0;
|
|
384
|
+
const totalHeight = headerH + (this.rows.length * rowHeight);
|
|
385
|
+
|
|
386
|
+
const bgColor = options.bgColor || '#18222d';
|
|
387
|
+
const borderColor = options.borderColor || '#2b3d4f';
|
|
388
|
+
const headerBg = options.headerBg || '#1c2836';
|
|
389
|
+
const labelColor = options.labelColor || '#90a4b7';
|
|
390
|
+
const valColor = options.valueColor || '#ffffff';
|
|
391
|
+
const linkColor = options.linkColor || '#5288c1';
|
|
392
|
+
const fontFamily = options.fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
393
|
+
|
|
394
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${totalHeight}" viewBox="0 0 ${width} ${totalHeight}">\n`;
|
|
395
|
+
svg += ` <style>\n`;
|
|
396
|
+
svg += ` .t-lbl { font-family: ${fontFamily}; font-size: 14px; fill: ${labelColor}; font-weight: 400; }\n`;
|
|
397
|
+
svg += ` .t-val { font-family: ${fontFamily}; font-size: 14px; fill: ${valColor}; font-weight: 400; }\n`;
|
|
398
|
+
svg += ` .t-val-bold { font-family: ${fontFamily}; font-size: 14px; fill: ${valColor}; font-weight: 600; }\n`;
|
|
399
|
+
svg += ` .t-link { font-family: ${fontFamily}; font-size: 14px; fill: ${linkColor}; font-weight: 500; cursor: pointer; }\n`;
|
|
400
|
+
svg += ` .t-hdr { font-family: ${fontFamily}; font-size: 14px; fill: #ffffff; font-weight: 700; }\n`;
|
|
401
|
+
svg += ` </style>\n`;
|
|
402
|
+
|
|
403
|
+
// Outer rounded card
|
|
404
|
+
svg += ` <rect x="0.5" y="0.5" width="${width - 1}" height="${totalHeight - 1}" rx="12" ry="12" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>\n`;
|
|
405
|
+
|
|
406
|
+
if (hasHeader) {
|
|
407
|
+
const clipId = `clip-top-${Math.floor(Math.random() * 1000000)}`;
|
|
408
|
+
svg += ` <clipPath id="${clipId}">\n`;
|
|
409
|
+
svg += ` <rect x="0.5" y="0.5" width="${width - 1}" height="${totalHeight - 1}" rx="12" ry="12" />\n`;
|
|
410
|
+
svg += ` </clipPath>\n`;
|
|
411
|
+
svg += ` <rect x="0.5" y="0.5" width="${width - 1}" height="${headerH}" fill="${headerBg}" clip-path="url(#${clipId})"/>\n`;
|
|
412
|
+
svg += ` <line x1="0" y1="${headerH}" x2="${width}" y2="${headerH}" stroke="${borderColor}" stroke-width="1"/>\n`;
|
|
413
|
+
|
|
414
|
+
const h1 = this.headers[0] ?? '';
|
|
415
|
+
const h2 = this.headers[1] ?? '';
|
|
416
|
+
svg += ` <text x="16" y="${Math.round(headerH / 2 + 5)}" class="t-hdr">${escapeSvg(h1)}</text>\n`;
|
|
417
|
+
if (h2) {
|
|
418
|
+
svg += ` <text x="${col1Width + 16}" y="${Math.round(headerH / 2 + 5)}" class="t-hdr">${escapeSvg(h2)}</text>\n`;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Vertical column divider
|
|
423
|
+
svg += ` <line x1="${col1Width}" y1="0" x2="${col1Width}" y2="${totalHeight}" stroke="${borderColor}" stroke-width="1"/>\n`;
|
|
424
|
+
|
|
425
|
+
// Data rows
|
|
426
|
+
for (let i = 0; i < this.rows.length; i++) {
|
|
427
|
+
const row = this.rows[i];
|
|
428
|
+
const y = headerH + (i * rowHeight);
|
|
429
|
+
|
|
430
|
+
if (i > 0 || hasHeader) {
|
|
431
|
+
svg += ` <line x1="0" y1="${y}" x2="${width}" y2="${y}" stroke="${borderColor}" stroke-width="1"/>\n`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const textY = y + Math.round(rowHeight / 2 + 5);
|
|
435
|
+
const col1Val = row[0] !== undefined ? String(row[0]) : '';
|
|
436
|
+
const col2Val = row[1] !== undefined ? String(row[1]) : '';
|
|
437
|
+
|
|
438
|
+
svg += ` <text x="16" y="${textY}" class="t-lbl">${escapeSvg(col1Val)}</text>\n`;
|
|
439
|
+
|
|
440
|
+
if (col2Val) {
|
|
441
|
+
const isLink = col2Val.startsWith('@') || col2Val.startsWith('tg://') || col2Val.startsWith('http');
|
|
442
|
+
const isBold = options.boldValues === true || (options.boldValues !== false && (
|
|
443
|
+
['telegraf.js', 'telegix', 'free user', 'premium', 'active', 'online', 'pro', 'connected', 'operational', 'success', 'ok'].includes(col2Val.toLowerCase()) ||
|
|
444
|
+
i === 0 ||
|
|
445
|
+
!isNaN(Number(col2Val))
|
|
446
|
+
));
|
|
447
|
+
const cls = isLink ? 't-link' : (isBold ? 't-val-bold' : 't-val');
|
|
448
|
+
svg += ` <text x="${col1Width + 16}" y="${textY}" class="${cls}">${escapeSvg(col2Val)}</text>\n`;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
svg += `</svg>`;
|
|
453
|
+
return svg;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Convert SVG table to Buffer (Node.js) or Uint8Array
|
|
458
|
+
* @param {object} [options]
|
|
459
|
+
* @returns {Buffer|Uint8Array}
|
|
460
|
+
*/
|
|
461
|
+
toBuffer(options = {}) {
|
|
462
|
+
const svg = this.toCardSvg(options);
|
|
463
|
+
if (typeof Buffer !== 'undefined') {
|
|
464
|
+
return Buffer.from(svg, 'utf-8');
|
|
465
|
+
}
|
|
466
|
+
return new TextEncoder().encode(svg);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Convert SVG table to base64 Data URL
|
|
471
|
+
* @param {object} [options]
|
|
472
|
+
* @returns {string}
|
|
473
|
+
*/
|
|
474
|
+
toDataUrl(options = {}) {
|
|
475
|
+
const svg = this.toCardSvg(options);
|
|
476
|
+
if (typeof Buffer !== 'undefined') {
|
|
477
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg, 'utf-8').toString('base64')}`;
|
|
478
|
+
}
|
|
479
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Convert to Telegram Bot API 10.3 InputRichBlockTable / RichBlockTable payload
|
|
484
|
+
* @returns {object}
|
|
485
|
+
*/
|
|
486
|
+
toRichBlock() {
|
|
487
|
+
return {
|
|
488
|
+
type: 'table',
|
|
489
|
+
is_bordered: this.is_bordered,
|
|
490
|
+
is_compact: this.is_compact,
|
|
491
|
+
is_striped: this.is_striped,
|
|
492
|
+
cells: this.toCells(),
|
|
493
|
+
...(this.caption ? { caption: this.caption } : {}),
|
|
494
|
+
...(this.headers.length > 0 ? { headers: this.headers } : {}),
|
|
495
|
+
rows: this.rows,
|
|
496
|
+
...(this._title ? { title: this._title } : {}),
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* JSON serialization for Bot API 10.3
|
|
502
|
+
*/
|
|
503
|
+
toJSON() {
|
|
504
|
+
return this.toRichBlock();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ==========================================
|
|
508
|
+
// Static Factory & Formatting Methods
|
|
509
|
+
// ==========================================
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Factory method to create a new Table instance
|
|
513
|
+
* @param {object} [options]
|
|
514
|
+
* @returns {Table}
|
|
515
|
+
*/
|
|
516
|
+
static create(options) {
|
|
517
|
+
return new Table(options);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Create Table from array of JavaScript objects
|
|
522
|
+
* @param {Array<object>} array
|
|
523
|
+
* @param {Array<string>} [columns] - Optional specific columns or keys
|
|
524
|
+
* @param {object} [options]
|
|
525
|
+
* @returns {Table}
|
|
526
|
+
*/
|
|
527
|
+
static fromObjects(array, columns, options = {}) {
|
|
528
|
+
if (!Array.isArray(array) || array.length === 0) {
|
|
529
|
+
return new Table(options);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const cols = columns && columns.length > 0
|
|
533
|
+
? columns
|
|
534
|
+
: Object.keys(array[0]);
|
|
535
|
+
|
|
536
|
+
const table = new Table({
|
|
537
|
+
headers: cols.map((c) => c.charAt(0).toUpperCase() + c.slice(1).replace(/_/g, ' ')),
|
|
538
|
+
...options,
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
for (const item of array) {
|
|
542
|
+
const row = cols.map((c) => item[c]);
|
|
543
|
+
table.row(row);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return table;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Format headers and rows into a formatted string
|
|
551
|
+
* @param {Array<string>} headers
|
|
552
|
+
* @param {Array<Array<any>>} rows
|
|
553
|
+
* @param {object} [options]
|
|
554
|
+
* @param {string} [options.style='box'] - 'box', 'ascii', 'compact', 'clean', 'markdown'
|
|
555
|
+
* @param {boolean} [options.isCompact=false]
|
|
556
|
+
* @param {Array<'left'|'center'|'right'>} [options.alignments]
|
|
557
|
+
* @returns {string}
|
|
558
|
+
*/
|
|
559
|
+
static format(headers = [], rows = [], options = {}) {
|
|
560
|
+
const opts = typeof options === 'string' ? { style: options } : (options || {});
|
|
561
|
+
const styleName = opts.style || (opts.isCompact || opts.is_compact ? 'compact' : 'box');
|
|
562
|
+
|
|
563
|
+
if (styleName === 'markdown') {
|
|
564
|
+
return Table.markdown(headers, rows, opts);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const border = STYLES[styleName] || STYLES.box;
|
|
568
|
+
const alignments = opts.alignments || [];
|
|
569
|
+
const isCompact = Boolean(opts.isCompact || opts.is_compact);
|
|
570
|
+
|
|
571
|
+
const numCols = Math.max(
|
|
572
|
+
headers.length,
|
|
573
|
+
...rows.map((r) => (Array.isArray(r) ? r.length : 0)),
|
|
574
|
+
1
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
// Calculate maximum width for each column
|
|
578
|
+
const colWidths = new Array(numCols).fill(0);
|
|
579
|
+
|
|
580
|
+
for (let c = 0; c < numCols; c++) {
|
|
581
|
+
if (headers[c] !== undefined) {
|
|
582
|
+
colWidths[c] = Math.max(colWidths[c], visualLength(headers[c]));
|
|
583
|
+
}
|
|
584
|
+
for (const row of rows) {
|
|
585
|
+
if (row && row[c] !== undefined) {
|
|
586
|
+
colWidths[c] = Math.max(colWidths[c], visualLength(row[c]));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Minimum column width of 1 character
|
|
590
|
+
colWidths[c] = Math.max(colWidths[c], 1);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const padding = isCompact ? 0 : 1;
|
|
594
|
+
const padChar = ' ';
|
|
595
|
+
const lines = [];
|
|
596
|
+
|
|
597
|
+
// Helper to format a cell with padding
|
|
598
|
+
const formatCell = (val, colIdx) => {
|
|
599
|
+
const w = colWidths[colIdx];
|
|
600
|
+
const align = alignments[colIdx] || 'left';
|
|
601
|
+
const text = pad(val, w, align);
|
|
602
|
+
return isCompact ? text : `${padChar}${text}${padChar}`;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
// Helper to build a horizontal border row
|
|
606
|
+
const buildBorder = (left, mid, right, horiz) => {
|
|
607
|
+
if (!left && !mid && !right) return '';
|
|
608
|
+
const parts = colWidths.map((w) => horiz.repeat(w + (isCompact ? 0 : 2)));
|
|
609
|
+
return `${left}${parts.join(mid)}${right}`;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Top border
|
|
613
|
+
if (border.topLeft || border.topMid || border.topRight) {
|
|
614
|
+
const topRow = buildBorder(border.topLeft, border.topMid, border.topRight, border.horizontal);
|
|
615
|
+
if (topRow) lines.push(topRow);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Headers row
|
|
619
|
+
if (headers.length > 0) {
|
|
620
|
+
const headerCells = [];
|
|
621
|
+
for (let c = 0; c < numCols; c++) {
|
|
622
|
+
headerCells.push(formatCell(headers[c] ?? '', c));
|
|
623
|
+
}
|
|
624
|
+
lines.push(`${border.vertical}${headerCells.join(border.vertical)}${border.vertical}`);
|
|
625
|
+
|
|
626
|
+
// Mid separator row
|
|
627
|
+
if (border.midLeft || border.midMid || border.midRight || border.horizontal) {
|
|
628
|
+
const midRow = buildBorder(border.midLeft, border.midMid, border.midRight, border.horizontal);
|
|
629
|
+
if (midRow) lines.push(midRow);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Data rows
|
|
634
|
+
for (let r = 0; r < rows.length; r++) {
|
|
635
|
+
const row = rows[r] || [];
|
|
636
|
+
const cells = [];
|
|
637
|
+
for (let c = 0; c < numCols; c++) {
|
|
638
|
+
cells.push(formatCell(row[c] ?? '', c));
|
|
639
|
+
}
|
|
640
|
+
lines.push(`${border.vertical}${cells.join(border.vertical)}${border.vertical}`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Bottom border
|
|
644
|
+
if (border.bottomLeft || border.bottomMid || border.bottomRight) {
|
|
645
|
+
const bottomRow = buildBorder(border.bottomLeft, border.bottomMid, border.bottomRight, border.horizontal);
|
|
646
|
+
if (bottomRow) lines.push(bottomRow);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return lines.join('\n');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Shortcut for box Unicode table
|
|
654
|
+
* @param {Array<string>} headers
|
|
655
|
+
* @param {Array<Array<any>>} rows
|
|
656
|
+
* @param {object} [options]
|
|
657
|
+
*/
|
|
658
|
+
static box(headers, rows, options = {}) {
|
|
659
|
+
return Table.format(headers, rows, { ...options, style: 'box' });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Shortcut for ASCII table (+----+----+ etc.)
|
|
664
|
+
* @param {Array<string>} headers
|
|
665
|
+
* @param {Array<Array<any>>} rows
|
|
666
|
+
* @param {object} [options]
|
|
667
|
+
*/
|
|
668
|
+
static ascii(headers, rows, options = {}) {
|
|
669
|
+
return Table.format(headers, rows, { ...options, style: 'ascii' });
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Shortcut for compact table
|
|
674
|
+
* @param {Array<string>} headers
|
|
675
|
+
* @param {Array<Array<any>>} rows
|
|
676
|
+
* @param {object} [options]
|
|
677
|
+
*/
|
|
678
|
+
static compact(headers, rows, options = {}) {
|
|
679
|
+
return Table.format(headers, rows, { ...options, style: 'compact', isCompact: true });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Generate Markdown table format (| Col 1 | Col 2 |)
|
|
684
|
+
* @param {Array<string>} headers
|
|
685
|
+
* @param {Array<Array<any>>} rows
|
|
686
|
+
* @param {object} [options]
|
|
687
|
+
*/
|
|
688
|
+
static markdown(headers = [], rows = [], options = {}) {
|
|
689
|
+
const alignments = options.alignments || [];
|
|
690
|
+
const numCols = Math.max(
|
|
691
|
+
headers.length,
|
|
692
|
+
...rows.map((r) => (Array.isArray(r) ? r.length : 0)),
|
|
693
|
+
1
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
const colWidths = new Array(numCols).fill(3);
|
|
697
|
+
for (let c = 0; c < numCols; c++) {
|
|
698
|
+
if (headers[c] !== undefined) {
|
|
699
|
+
colWidths[c] = Math.max(colWidths[c], visualLength(headers[c]));
|
|
700
|
+
}
|
|
701
|
+
for (const row of rows) {
|
|
702
|
+
if (row && row[c] !== undefined) {
|
|
703
|
+
colWidths[c] = Math.max(colWidths[c], visualLength(row[c]));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const lines = [];
|
|
709
|
+
|
|
710
|
+
// Header
|
|
711
|
+
const headerCells = [];
|
|
712
|
+
for (let c = 0; c < numCols; c++) {
|
|
713
|
+
headerCells.push(pad(headers[c] ?? '', colWidths[c], 'left'));
|
|
714
|
+
}
|
|
715
|
+
lines.push(`| ${headerCells.join(' | ')} |`);
|
|
716
|
+
|
|
717
|
+
// Separator with alignment indicators
|
|
718
|
+
const sepCells = [];
|
|
719
|
+
for (let c = 0; c < numCols; c++) {
|
|
720
|
+
const align = alignments[c] || 'left';
|
|
721
|
+
const w = colWidths[c];
|
|
722
|
+
if (align === 'center') {
|
|
723
|
+
sepCells.push(`:${'-'.repeat(Math.max(w - 2, 1))}:`);
|
|
724
|
+
} else if (align === 'right') {
|
|
725
|
+
sepCells.push(`${'-'.repeat(Math.max(w - 1, 1))}:`);
|
|
726
|
+
} else {
|
|
727
|
+
sepCells.push(`:${'-'.repeat(Math.max(w - 1, 1))}`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
lines.push(`| ${sepCells.join(' | ')} |`);
|
|
731
|
+
|
|
732
|
+
// Rows
|
|
733
|
+
for (const row of rows) {
|
|
734
|
+
const cells = [];
|
|
735
|
+
for (let c = 0; c < numCols; c++) {
|
|
736
|
+
const align = alignments[c] || 'left';
|
|
737
|
+
cells.push(pad(row[c] ?? '', colWidths[c], align));
|
|
738
|
+
}
|
|
739
|
+
lines.push(`| ${cells.join(' | ')} |`);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return lines.join('\n');
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Helper to format table directly into HTML with <pre> tag for Telegram
|
|
747
|
+
* @param {Array<string>} headers
|
|
748
|
+
* @param {Array<Array<any>>} rows
|
|
749
|
+
* @param {object} [options]
|
|
750
|
+
*/
|
|
751
|
+
static html(headers, rows, options = {}) {
|
|
752
|
+
const formatted = Table.format(headers, rows, options);
|
|
753
|
+
const title = options.title ? `<b>${escapeHtml(options.title)}</b>\n\n` : '';
|
|
754
|
+
return `${title}<pre>${escapeHtml(formatted)}</pre>`;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Create a Card Table matching modern Telegram Bot Card Table UI
|
|
759
|
+
* @param {Array<string>} headers
|
|
760
|
+
* @param {Array<Array<any>>} rows
|
|
761
|
+
* @param {object} [options]
|
|
762
|
+
* @returns {Table}
|
|
763
|
+
*/
|
|
764
|
+
static card(headers, rows = [], options = {}) {
|
|
765
|
+
return new Table(headers, rows, {
|
|
766
|
+
style: 'card',
|
|
767
|
+
is_bordered: true,
|
|
768
|
+
...options,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Create a pre-configured System Status Card Table (matching Telegram bot screenshot)
|
|
774
|
+
* @param {object} [data]
|
|
775
|
+
* @param {object} [options]
|
|
776
|
+
* @returns {Table}
|
|
777
|
+
*/
|
|
778
|
+
static systemStatus(data = {}, options = {}) {
|
|
779
|
+
const defaultData = {
|
|
780
|
+
engine: 'Telegix',
|
|
781
|
+
runtime: '0h 19m 32s',
|
|
782
|
+
node: typeof process !== 'undefined' && process.version ? process.version : 'v23.11',
|
|
783
|
+
features: 514,
|
|
784
|
+
groups: 168,
|
|
785
|
+
users: 9528,
|
|
786
|
+
...data,
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
return new Table(
|
|
790
|
+
['🤖 SYSTEM', 'Status'],
|
|
791
|
+
[
|
|
792
|
+
['Engine', defaultData.engine],
|
|
793
|
+
['Runtime', defaultData.runtime],
|
|
794
|
+
['Node', defaultData.node],
|
|
795
|
+
['Features', defaultData.features],
|
|
796
|
+
['Groups', defaultData.groups],
|
|
797
|
+
['Users', defaultData.users],
|
|
798
|
+
],
|
|
799
|
+
{
|
|
800
|
+
style: 'card',
|
|
801
|
+
is_bordered: true,
|
|
802
|
+
title: options.title || '',
|
|
803
|
+
...options,
|
|
804
|
+
}
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Create a pre-configured User Profile Card Table (matching Telegram bot screenshot)
|
|
810
|
+
* @param {object} [data]
|
|
811
|
+
* @param {object} [options]
|
|
812
|
+
* @returns {Table}
|
|
813
|
+
*/
|
|
814
|
+
static userProfile(data = {}, options = {}) {
|
|
815
|
+
const defaultData = {
|
|
816
|
+
username: '@seventynn',
|
|
817
|
+
status: 'Free User',
|
|
818
|
+
limit: 0,
|
|
819
|
+
points: 0,
|
|
820
|
+
time: 'Selasa, 8 September 2026',
|
|
821
|
+
...data,
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
return new Table(
|
|
825
|
+
['👤 PROFILE', 'Info'],
|
|
826
|
+
[
|
|
827
|
+
['Username', defaultData.username],
|
|
828
|
+
['Status', defaultData.status],
|
|
829
|
+
['Limit', defaultData.limit],
|
|
830
|
+
['Points', defaultData.points],
|
|
831
|
+
['Time', defaultData.time],
|
|
832
|
+
],
|
|
833
|
+
{
|
|
834
|
+
style: 'card',
|
|
835
|
+
is_bordered: true,
|
|
836
|
+
title: options.title || '',
|
|
837
|
+
...options,
|
|
838
|
+
}
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Render multiple stacked card tables into a single SVG graphic
|
|
844
|
+
* (e.g. Card 1 SYSTEM and Card 2 PROFILE like in the Telegram bot screenshot)
|
|
845
|
+
* @param {Array<Table|object>} tables
|
|
846
|
+
* @param {object} [options]
|
|
847
|
+
* @returns {string} SVG XML markup string
|
|
848
|
+
*/
|
|
849
|
+
static multiCardSvg(tables, options = {}) {
|
|
850
|
+
const list = Array.isArray(tables) ? tables : [tables];
|
|
851
|
+
const width = Number(options.width || 420);
|
|
852
|
+
const gap = Number(options.gap || 14);
|
|
853
|
+
const padding = Number(options.padding || 0);
|
|
854
|
+
|
|
855
|
+
const instances = list.map((t) => {
|
|
856
|
+
if (t instanceof Table) return t;
|
|
857
|
+
if (t && typeof t.toCardSvg === 'function') return t;
|
|
858
|
+
if (t && Array.isArray(t.headers) && Array.isArray(t.rows)) return new Table(t);
|
|
859
|
+
if (Array.isArray(t)) return new Table(t[0], t[1]);
|
|
860
|
+
return new Table(t);
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
let currentY = padding;
|
|
864
|
+
const renderedParts = [];
|
|
865
|
+
|
|
866
|
+
for (let idx = 0; idx < instances.length; idx++) {
|
|
867
|
+
const t = instances[idx];
|
|
868
|
+
const headerH = t.headers.length > 0 ? (options.headerHeight || 38) : 0;
|
|
869
|
+
const tHeight = headerH + (t.rows.length * (options.rowHeight || 36));
|
|
870
|
+
const col1Width = Number(options.col1Width || t.col1Width || Math.round(width * 0.36));
|
|
871
|
+
const headerHeight = Number(options.headerHeight || 38);
|
|
872
|
+
const rowHeight = Number(options.rowHeight || 36);
|
|
873
|
+
const hasHeader = t.headers.length > 0;
|
|
874
|
+
|
|
875
|
+
const bgColor = options.bgColor || '#18222d';
|
|
876
|
+
const borderColor = options.borderColor || '#2b3d4f';
|
|
877
|
+
const headerBg = options.headerBg || '#1c2836';
|
|
878
|
+
|
|
879
|
+
let g = ` <g transform="translate(${padding}, ${currentY})">\n`;
|
|
880
|
+
g += ` <rect x="0.5" y="0.5" width="${width - 1}" height="${tHeight - 1}" rx="12" ry="12" fill="${bgColor}" stroke="${borderColor}" stroke-width="1"/>\n`;
|
|
881
|
+
|
|
882
|
+
if (hasHeader) {
|
|
883
|
+
const clipId = `multi-clip-${idx}-${currentY}`;
|
|
884
|
+
g += ` <clipPath id="${clipId}">\n`;
|
|
885
|
+
g += ` <rect x="0.5" y="0.5" width="${width - 1}" height="${tHeight - 1}" rx="12" ry="12" />\n`;
|
|
886
|
+
g += ` </clipPath>\n`;
|
|
887
|
+
g += ` <rect x="0.5" y="0.5" width="${width - 1}" height="${headerH}" fill="${headerBg}" clip-path="url(#${clipId})"/>\n`;
|
|
888
|
+
g += ` <line x1="0" y1="${headerH}" x2="${width}" y2="${headerH}" stroke="${borderColor}" stroke-width="1"/>\n`;
|
|
889
|
+
|
|
890
|
+
const h1 = t.headers[0] ?? '';
|
|
891
|
+
const h2 = t.headers[1] ?? '';
|
|
892
|
+
g += ` <text x="16" y="${Math.round(headerH / 2 + 5)}" class="t-hdr">${escapeSvg(h1)}</text>\n`;
|
|
893
|
+
if (h2) {
|
|
894
|
+
g += ` <text x="${col1Width + 16}" y="${Math.round(headerH / 2 + 5)}" class="t-hdr">${escapeSvg(h2)}</text>\n`;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
g += ` <line x1="${col1Width}" y1="0" x2="${col1Width}" y2="${tHeight}" stroke="${borderColor}" stroke-width="1"/>\n`;
|
|
899
|
+
|
|
900
|
+
for (let i = 0; i < t.rows.length; i++) {
|
|
901
|
+
const row = t.rows[i];
|
|
902
|
+
const y = headerH + (i * rowHeight);
|
|
903
|
+
|
|
904
|
+
if (i > 0 || hasHeader) {
|
|
905
|
+
g += ` <line x1="0" y1="${y}" x2="${width}" y2="${y}" stroke="${borderColor}" stroke-width="1"/>\n`;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const textY = y + Math.round(rowHeight / 2 + 5);
|
|
909
|
+
const col1Val = row[0] !== undefined ? String(row[0]) : '';
|
|
910
|
+
const col2Val = row[1] !== undefined ? String(row[1]) : '';
|
|
911
|
+
|
|
912
|
+
g += ` <text x="16" y="${textY}" class="t-lbl">${escapeSvg(col1Val)}</text>\n`;
|
|
913
|
+
|
|
914
|
+
if (col2Val) {
|
|
915
|
+
const isLink = col2Val.startsWith('@') || col2Val.startsWith('tg://') || col2Val.startsWith('http');
|
|
916
|
+
const isBold = options.boldValues !== false && (
|
|
917
|
+
['Telegraf.js', 'Telegix', 'Free User', 'Premium', 'Active', 'Online', 'PRO'].includes(col2Val) ||
|
|
918
|
+
i === 0
|
|
919
|
+
);
|
|
920
|
+
const cls = isLink ? 't-link' : (isBold ? 't-val-bold' : 't-val');
|
|
921
|
+
g += ` <text x="${col1Width + 16}" y="${textY}" class="${cls}">${escapeSvg(col2Val)}</text>\n`;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
g += ` </g>\n`;
|
|
925
|
+
|
|
926
|
+
renderedParts.push(g);
|
|
927
|
+
currentY += tHeight + gap;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const totalWidth = width + (padding * 2);
|
|
931
|
+
const totalHeight = currentY - gap + padding;
|
|
932
|
+
const fontFamily = options.fontFamily || '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif';
|
|
933
|
+
const labelColor = options.labelColor || '#90a4b7';
|
|
934
|
+
const valColor = options.valueColor || '#ffffff';
|
|
935
|
+
const linkColor = options.linkColor || '#5288c1';
|
|
936
|
+
|
|
937
|
+
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="${totalHeight}" viewBox="0 0 ${totalWidth} ${totalHeight}">\n`;
|
|
938
|
+
svg += ` <style>\n`;
|
|
939
|
+
svg += ` .t-lbl { font-family: ${fontFamily}; font-size: 14px; fill: ${labelColor}; font-weight: 400; }\n`;
|
|
940
|
+
svg += ` .t-val { font-family: ${fontFamily}; font-size: 14px; fill: ${valColor}; font-weight: 400; }\n`;
|
|
941
|
+
svg += ` .t-val-bold { font-family: ${fontFamily}; font-size: 14px; fill: ${valColor}; font-weight: 600; }\n`;
|
|
942
|
+
svg += ` .t-link { font-family: ${fontFamily}; font-size: 14px; fill: ${linkColor}; font-weight: 500; cursor: pointer; }\n`;
|
|
943
|
+
svg += ` .t-hdr { font-family: ${fontFamily}; font-size: 14px; fill: #ffffff; font-weight: 700; }\n`;
|
|
944
|
+
svg += ` </style>\n`;
|
|
945
|
+
|
|
946
|
+
svg += renderedParts.join('\n');
|
|
947
|
+
svg += `</svg>`;
|
|
948
|
+
return svg;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Telegram Bot API 10.3 InputRichBlockTable class
|
|
954
|
+
* Represents a structured table block in rich messages
|
|
955
|
+
*/
|
|
956
|
+
export class InputRichBlockTable {
|
|
957
|
+
/**
|
|
958
|
+
* @param {Array<string>|object} [headersOrOptions]
|
|
959
|
+
* @param {Array<Array<any>>} [rows]
|
|
960
|
+
* @param {object} [options]
|
|
961
|
+
*/
|
|
962
|
+
constructor(headersOrOptions = [], rows = [], options = {}) {
|
|
963
|
+
this.type = 'table';
|
|
964
|
+
if (headersOrOptions && !Array.isArray(headersOrOptions) && typeof headersOrOptions === 'object') {
|
|
965
|
+
const opt = headersOrOptions;
|
|
966
|
+
this.headers = opt.headers || [];
|
|
967
|
+
this.rows = opt.rows || [];
|
|
968
|
+
this.is_compact = Boolean(opt.is_compact ?? opt.isCompact ?? false);
|
|
969
|
+
this.is_bordered = Boolean(opt.is_bordered ?? opt.isBordered ?? true);
|
|
970
|
+
this.is_striped = Boolean(opt.is_striped ?? opt.isStriped ?? false);
|
|
971
|
+
this.caption = opt.caption || '';
|
|
972
|
+
this.alignments = opt.alignments || [];
|
|
973
|
+
this.title = opt.title || '';
|
|
974
|
+
this.style = opt.style || (this.is_compact ? 'compact' : 'box');
|
|
975
|
+
this.col1Width = opt.col1Width;
|
|
976
|
+
} else {
|
|
977
|
+
this.headers = Array.isArray(headersOrOptions) ? [...headersOrOptions] : [];
|
|
978
|
+
this.rows = Array.isArray(rows) ? rows.map((r) => [...r]) : [];
|
|
979
|
+
this.is_compact = Boolean(options.is_compact ?? options.isCompact ?? false);
|
|
980
|
+
this.is_bordered = Boolean(options.is_bordered ?? options.isBordered ?? true);
|
|
981
|
+
this.is_striped = Boolean(options.is_striped ?? options.isStriped ?? false);
|
|
982
|
+
this.caption = options.caption || '';
|
|
983
|
+
this.alignments = options.alignments || [];
|
|
984
|
+
this.title = options.title || '';
|
|
985
|
+
this.style = options.style || (this.is_compact ? 'compact' : 'box');
|
|
986
|
+
this.col1Width = options.col1Width;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Set compact mode
|
|
992
|
+
* @param {boolean} [isCompact=true]
|
|
993
|
+
* @returns {this}
|
|
994
|
+
*/
|
|
995
|
+
compact(isCompact = true) {
|
|
996
|
+
this.is_compact = Boolean(isCompact);
|
|
997
|
+
return this;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Set bordered mode
|
|
1002
|
+
* @param {boolean} [isBordered=true]
|
|
1003
|
+
* @returns {this}
|
|
1004
|
+
*/
|
|
1005
|
+
bordered(isBordered = true) {
|
|
1006
|
+
this.is_bordered = Boolean(isBordered);
|
|
1007
|
+
return this;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Set striped mode
|
|
1012
|
+
* @param {boolean} [isStriped=true]
|
|
1013
|
+
* @returns {this}
|
|
1014
|
+
*/
|
|
1015
|
+
striped(isStriped = true) {
|
|
1016
|
+
this.is_striped = Boolean(isStriped);
|
|
1017
|
+
return this;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Add a row of cells
|
|
1022
|
+
* @param {...any} cells
|
|
1023
|
+
* @returns {this}
|
|
1024
|
+
*/
|
|
1025
|
+
addRow(...cells) {
|
|
1026
|
+
if (cells.length === 1 && Array.isArray(cells[0])) {
|
|
1027
|
+
this.rows.push([...cells[0]]);
|
|
1028
|
+
} else {
|
|
1029
|
+
this.rows.push(cells.flat());
|
|
1030
|
+
}
|
|
1031
|
+
return this;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Convert table data into 2D array of RichBlockTableCell for Bot API 10.3
|
|
1036
|
+
*/
|
|
1037
|
+
toCells() {
|
|
1038
|
+
const cells = [];
|
|
1039
|
+
if (this.headers.length > 0) {
|
|
1040
|
+
cells.push(
|
|
1041
|
+
this.headers.map((h, i) => ({
|
|
1042
|
+
text: String(h ?? ''),
|
|
1043
|
+
is_header: true,
|
|
1044
|
+
align: this.alignments[i] || 'center',
|
|
1045
|
+
valign: 'middle',
|
|
1046
|
+
}))
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
for (const row of this.rows) {
|
|
1050
|
+
cells.push(
|
|
1051
|
+
row.map((cell, i) => {
|
|
1052
|
+
if (cell && typeof cell === 'object' && cell.text !== undefined) {
|
|
1053
|
+
return {
|
|
1054
|
+
align: this.alignments[i] || 'left',
|
|
1055
|
+
valign: 'middle',
|
|
1056
|
+
...cell,
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
text: String(cell ?? ''),
|
|
1061
|
+
align: this.alignments[i] || 'left',
|
|
1062
|
+
valign: 'middle',
|
|
1063
|
+
};
|
|
1064
|
+
})
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
return cells;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Render table as SVG Card
|
|
1072
|
+
* @param {object} [options]
|
|
1073
|
+
* @returns {string}
|
|
1074
|
+
*/
|
|
1075
|
+
toCardSvg(options = {}) {
|
|
1076
|
+
const t = new Table({
|
|
1077
|
+
headers: this.headers,
|
|
1078
|
+
rows: this.rows,
|
|
1079
|
+
col1Width: this.col1Width,
|
|
1080
|
+
...options,
|
|
1081
|
+
});
|
|
1082
|
+
return t.toCardSvg(options);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Render HTML representation
|
|
1087
|
+
*/
|
|
1088
|
+
toHtml(options = {}) {
|
|
1089
|
+
const tableStr = Table.format(this.headers, this.rows, {
|
|
1090
|
+
style: options.style || this.style,
|
|
1091
|
+
isCompact: this.is_compact,
|
|
1092
|
+
alignments: this.alignments,
|
|
1093
|
+
...options,
|
|
1094
|
+
});
|
|
1095
|
+
const titleHtml = this.title ? `<b>${escapeHtml(this.title)}</b>\n\n` : '';
|
|
1096
|
+
return `${titleHtml}<pre>${escapeHtml(tableStr)}</pre>`;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Convert to Bot API 10.3 JSON payload
|
|
1101
|
+
*/
|
|
1102
|
+
toJSON() {
|
|
1103
|
+
return {
|
|
1104
|
+
type: 'table',
|
|
1105
|
+
is_bordered: this.is_bordered,
|
|
1106
|
+
is_compact: this.is_compact,
|
|
1107
|
+
is_striped: this.is_striped,
|
|
1108
|
+
cells: this.toCells(),
|
|
1109
|
+
...(this.caption ? { caption: this.caption } : {}),
|
|
1110
|
+
...(this.headers.length > 0 ? { headers: this.headers } : {}),
|
|
1111
|
+
rows: this.rows,
|
|
1112
|
+
...(this.title ? { title: this.title } : {}),
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
static create(headers, rows, options) {
|
|
1117
|
+
return new InputRichBlockTable(headers, rows, options);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/**
|
|
1122
|
+
* Alias for Telegram Bot API 10.3 RichBlockTable
|
|
1123
|
+
*/
|
|
1124
|
+
export const RichBlockTable = InputRichBlockTable;
|