import type { AutoColumnConfig, AutoRowConfig, RowConfig, TableConfig, } from './ColumnConfig'; import TagHelper from './TagHelper'; /** * Helper calss to create printable design foor RNPrinter * * @export * @class DesignBuilder */ export default class DesignBuilder { private maxChar: number; private _design: string[] = []; /** * Creates an instance of DesignBuilder. * @param {number} maxChar Max char per line during printing * @memberof DesignBuilder */ constructor(maxChar: number) { this.maxChar = maxChar; } private set design(lines: string) { this._design = lines.split('\n'); } /** * Get design result * * @readonly */ public get design(): string { return this._design.join('\n'); } /** * Get design array * * @readonly */ public get designs(): string[] { return this._design; } /** * Display design preview * */ public preview() { this.designs.forEach((line) => { console.log(line.trim()); }); } /** * Add formated line using TagHelper will add \n for every lines * * @param {string} line */ public addLine(line: string) { if (!line.endsWith('\n')) { line = TagHelper.line(line); } return this.addRawLine(line); } /** * Add raw formated line using TagHelper, not \n will be added * * @param {string} line */ public addRawLine(line: string) { this._design.push(line); return this; } /** * Add array of formated line using TagHelper * * @param {string} lines */ public addLines(lines: string[]) { lines.forEach((line) => this.addLine(line)); return this; } /** * Add one line of blank white space */ public addBlankLine() { this._design.push(' '); return this; } /** * Repeat single char for one row * * @param {string} [char='-'] char to repeat, default is **-** */ public drawSeparator(char: string = '-') { this.addLine( Array(this.maxChar) .fill(char[0] || ' ') .join('') ); return this; } private testWhitespace(char: string): boolean { var white = new RegExp(/^\s$/); return white.test(char.charAt(0)); } private padLeftRight(str: string, pad: string, length: number): string { // Check if the string is longer than n characters. if (str.length >= length) { return str; } // Calculate the number of characters to pad on each side. const padLength = Math.ceil((length - str.length) / 2); // Determine the number of characters to pad on the left and right sides. const left = padLength; let right = padLength; // Decrease the right padding length by one if the total length is greater than n. if (padLength * 2 + str.length > length) { right--; } // Create the padded string. return `${pad.repeat(left)}${str}${pad.repeat(right)}`; } /** * Split text so it can fit into max char config * * @param {string} text text to plit * @param {number} max char, default is current max char * @return {string[]} chunked text */ public chuckLines(text: string, maxLength: number = this.maxChar): string[] { // most of this function is credited too // https://stackoverflow.com/a/14487422/4825796 let newLineStr = '\n'; let updatedStr = ''; while (text.length > maxLength) { let found = false; const testPiece = text.substring(0, maxLength); if (testPiece.includes('\n')) { const enterIdex = testPiece.indexOf('\n'); updatedStr = updatedStr + text.slice(0, enterIdex + 1); text = text.slice(enterIdex + 1); } else { // Inserts new line at first whitespace of the line for (let i = maxLength - 1; i >= 0; i--) { if (this.testWhitespace(text.charAt(i))) { // check from back to front for a space updatedStr = updatedStr + [text.slice(0, i), newLineStr].join(''); text = text.slice(i + 1); found = true; break; } } // Inserts new line at maxWidth position, the word is too long to wrap if (!found) { updatedStr += [text.slice(0, maxLength), newLineStr].join(''); text = text.slice(maxLength); } } } updatedStr = updatedStr + text; return updatedStr.split('\n').map((l) => l.trim()); } /** * Create columned text, chuck each column * * @param {RowConfig} columns columns configuration * @return {*} {Array} chunked text */ public columns(columns: RowConfig): Array { const hasSpacer = columns.some((config) => config.spacer); const configs = columns.slice(0, 3); // only take maximum three columns let zero_width_count = 0; configs.forEach((column) => { if (!column.width) { zero_width_count += 1; } }); if (zero_width_count > 1) { console.warn( 'Invalid configuration!, thee are more than 1 column with 0 width' ); return []; } if (zero_width_count == 1) { // * If there are exatcly one column with 0 width, it will assume it takes the rest of width // * Get the ocupied width const ocupied = columns.reduce((total, column) => { if (column.width) { return (total += column.width + (hasSpacer ? 1 : 0)); } return total; }, 0); // * Get the rest of width const rest = this.maxChar - ocupied; // * Update the configuration columns = columns.map((column) => { if (!column.width) { column.width = rest; } return column; }); } // * Include spacer to all columns but the last column const totalWidth = configs.reduce((total, column) => { return total + column.width; }, 0) + (hasSpacer ? configs.length - 1 : 0); if (totalWidth > this.maxChar) { console.warn( `sum of column width ${totalWidth} exceed printing max char ${this.maxChar}\n`, 'If you add spacer, the total width will be added by (column_num - 1)' ); return []; } let result: Array> = []; let chunkedNums = 0; // * Chunks all texts configs.forEach((config) => { const width = config.width; const chunked = this.chuckLines(config.text || '', width); if (chunked.length > chunkedNums) { chunkedNums = chunked.length; } result.push(chunked); }); // * Pad result so all chunked array has same size // * Also pad line with whitespaces to emulate allignment const STRING_PADDER = ' '; result = result.map((chunkeds, index) => { const config = configs[index]!; const width = config.width; if (chunkeds.length < chunkedNums) { const diff = chunkedNums - chunkeds.length; const fill = Array(diff).fill(STRING_PADDER.repeat(config.width)); chunkeds.push(...fill); } chunkeds = chunkeds .map((line) => { if (line.length >= width) { return line; } switch (configs[index]?.allignment) { case TagHelper.ALLIGNMENT.LEFT: return line.padEnd(config.width, STRING_PADDER); case TagHelper.ALLIGNMENT.CENTER: return this.padLeftRight(line, STRING_PADDER, config.width); case TagHelper.ALLIGNMENT.RIGHT: return line.padStart(config.width, STRING_PADDER); } return line; }) .map((line) => { if (config.underline) { line = TagHelper.underline(line); } if (config.bold) { line = TagHelper.bold(line); } return line; }); return chunkeds; }); // Transpose array; result = Array.from(result[0]!).map((_, i) => result.map((row) => row[i]!)); const result2 = result.map((_) => { const line = _.join(hasSpacer ? ' ' : ''); return TagHelper.reset(line); }); return result2; } /** * Add table matrix texts to design * * @param {TableConfig} rows rows or table data generated by TableBuilder.build() * @return {*} {Array} chunked text */ public addTable(rows: TableConfig) { rows.forEach((row) => { this.addLines(this.columns(row)); }); return this; } /** * Add automated columned line the split width will be equal / automated and handled by the native side * * @param {RowConfig} columns the required attributes is { allignment, text } * @memberof DesignBuilder */ public addAutoColumn(columns: AutoRowConfig) { const configs = columns.slice(0, 3); // only take maximum three columns const line = configs.reduce( (construct: string, config: AutoColumnConfig) => { const text = config.text || ''; switch (config.allignment) { case TagHelper.ALLIGNMENT.LEFT: return construct + TagHelper.left(text); case TagHelper.ALLIGNMENT.CENTER: return construct + TagHelper.center(text); case TagHelper.ALLIGNMENT.RIGHT: return construct + TagHelper.right(text); } return ''; }, '' ); this.addLine(line); return this; } /** * Add standard printable characters */ public addPrintableCharacters() { this.addLines( this.chuckLines( 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=' ) ); return this; } }