From b949d4a6c61b3deac310c2f33c7f4c429ac88e44 Mon Sep 17 00:00:00 2001 From: Sean Sube Date: Sat, 14 Jun 2025 20:24:42 -0500 Subject: [PATCH] format utils for printing --- .../printer/__tests__/format-utils.test.ts | 125 ++++++++++++++++++ server/src/printer/format-utils.ts | 50 +++++++ server/src/printer/serial-printer.ts | 23 ++-- server/src/printer/test-printer.ts | 22 +-- 4 files changed, 195 insertions(+), 25 deletions(-) create mode 100644 server/src/printer/__tests__/format-utils.test.ts create mode 100644 server/src/printer/format-utils.ts diff --git a/server/src/printer/__tests__/format-utils.test.ts b/server/src/printer/__tests__/format-utils.test.ts new file mode 100644 index 0000000..2d61ac7 --- /dev/null +++ b/server/src/printer/__tests__/format-utils.test.ts @@ -0,0 +1,125 @@ +import { formatUtils } from '../format-utils'; + +describe('formatUtils', () => { + describe('createBanner', () => { + it('should create a banner with default length', () => { + const banner = formatUtils.createBanner('='); + expect(banner).toBe('='.repeat(40)); + }); + + it('should create a banner with custom length', () => { + const banner = formatUtils.createBanner('-', 32); + expect(banner).toBe('-'.repeat(32)); + }); + + it('should handle empty character', () => { + const banner = formatUtils.createBanner('', 10); + expect(banner).toBe(''); + }); + + it('should handle zero length', () => { + const banner = formatUtils.createBanner('*', 0); + expect(banner).toBe(''); + }); + }); + + describe('formatCheckbox', () => { + it('should format text with checkbox', () => { + const formatted = formatUtils.formatCheckbox('Test Item'); + expect(formatted).toBe('[ ] Test Item'); + }); + + it('should handle empty text', () => { + const formatted = formatUtils.formatCheckbox(''); + expect(formatted).toBe('[ ] '); + }); + + it('should handle text with special characters', () => { + const formatted = formatUtils.formatCheckbox('Test: Item (123)'); + expect(formatted).toBe('[ ] Test: Item (123)'); + }); + }); + + describe('formatList', () => { + it('should format list without numbering', () => { + const items = ['Item 1', 'Item 2', 'Item 3']; + const formatted = formatUtils.formatList(items); + expect(formatted).toEqual([ + '[ ] Item 1', + '[ ] Item 2', + '[ ] Item 3' + ]); + }); + + it('should format list with numbering', () => { + const items = ['Item 1', 'Item 2', 'Item 3']; + const formatted = formatUtils.formatList(items, 1); + expect(formatted).toEqual([ + '[ ] 1: Item 1', + '[ ] 2: Item 2', + '[ ] 3: Item 3' + ]); + }); + + it('should handle empty list', () => { + const formatted = formatUtils.formatList([]); + expect(formatted).toEqual([]); + }); + + it('should handle list with empty items', () => { + const items = ['', 'Item 2', '']; + const formatted = formatUtils.formatList(items); + expect(formatted).toEqual([ + '[ ] ', + '[ ] Item 2', + '[ ] ' + ]); + }); + }); + + describe('formatSection', () => { + it('should format section with default banner', () => { + const section = formatUtils.formatSection('Header', 'Content'); + expect(section).toEqual([ + '[ ] Header', + '='.repeat(40), + '', + 'Content', + '' + ]); + }); + + it('should format section with custom banner', () => { + const section = formatUtils.formatSection('Header', 'Content', '-'); + expect(section).toEqual([ + '[ ] Header', + '-'.repeat(40), + '', + 'Content', + '' + ]); + }); + + it('should handle empty content', () => { + const section = formatUtils.formatSection('Header', ''); + expect(section).toEqual([ + '[ ] Header', + '='.repeat(40), + '', + '', + '' + ]); + }); + + it('should handle empty header', () => { + const section = formatUtils.formatSection('', 'Content'); + expect(section).toEqual([ + '[ ] ', + '='.repeat(40), + '', + 'Content', + '' + ]); + }); + }); +}); \ No newline at end of file diff --git a/server/src/printer/format-utils.ts b/server/src/printer/format-utils.ts new file mode 100644 index 0000000..56981c7 --- /dev/null +++ b/server/src/printer/format-utils.ts @@ -0,0 +1,50 @@ +export const formatUtils = { + /** + * Creates a banner line with the specified character + * @param char Character to repeat + * @param length Length of the banner + * @returns Formatted banner string + */ + createBanner(char: string, length: number = 40): string { + return char.repeat(length); + }, + + /** + * Formats a checkbox item with the given text + * @param text Text to display after the checkbox + * @returns Formatted checkbox string + */ + formatCheckbox(text: string): string { + return `[ ] ${text}`; + }, + + /** + * Formats a list of items into lines with optional numbering + * @param items List of items to format + * @param startIndex Starting index for numbering (0 for no numbers) + * @returns Array of formatted lines + */ + formatList(items: string[], startIndex: number = 0): string[] { + return items.map((item, index) => { + const num = startIndex > 0 ? `${index + startIndex}: ` : ''; + return this.formatCheckbox(`${num}${item}`); + }); + }, + + /** + * Formats a section with a header and content + * @param header Section header + * @param content Section content + * @param bannerChar Character to use for the banner + * @returns Array of formatted lines + */ + formatSection(header: string, content: string, bannerChar: string = '='): string[] { + return [ + this.formatCheckbox(header), + this.createBanner(bannerChar), + '', + content, + '', + ]; + } +}; \ No newline at end of file diff --git a/server/src/printer/serial-printer.ts b/server/src/printer/serial-printer.ts index 97db9a8..d79628d 100644 --- a/server/src/printer/serial-printer.ts +++ b/server/src/printer/serial-printer.ts @@ -4,6 +4,7 @@ import { Task, Step, Printer as PrinterInterface } from '@shared/index'; import { StepRepository, PrintHistoryRepository } from '../db/repositories'; import { Knex } from 'knex'; import logger from '../logger'; +import { formatUtils } from './format-utils'; export class SerialPrinter implements PrinterInterface { private device: USB | null = null; @@ -57,8 +58,8 @@ export class SerialPrinter implements PrinterInterface { .align('ct') .style('b') .size(1, 1) // Normal size (0.08 x 2.13 mm) - .text(`[ ] Task: ${task.name}`) - .text('='.repeat(32)) + .text(formatUtils.formatCheckbox(`Task: ${task.name}`)) + .text(formatUtils.createBanner('=', 32)) .text('') .align('lt'); @@ -71,12 +72,14 @@ export class SerialPrinter implements PrinterInterface { // Print steps for (let i = 0; i < taskSteps.length; i++) { const step = taskSteps[i]; + const stepSection = formatUtils.formatSection(`Step ${i + 1}: ${step.name}`, step.instructions, '-'); + await this.printer .size(1, 1) // Normal size for step header - .text(`[ ] Step ${i + 1}: ${step.name}`) - .text('-'.repeat(32)) - .size(0, 0) // Smaller size for instructions (0.08 x 2.13 mm) - .text(step.instructions) + .text(stepSection[0]) // Header with checkbox + .text(stepSection[1]) // Banner + .size(0, 0) // Smaller size for instructions + .text(stepSection[3]) // Instructions .text(''); // Print step ID as barcode @@ -110,17 +113,19 @@ export class SerialPrinter implements PrinterInterface { } try { + const stepSection = formatUtils.formatSection(`Step: ${step.name}`, step.instructions); + await this.printer .font('a') .align('ct') .style('b') .size(1, 1) // Normal size (0.08 x 2.13 mm) - .text(`[ ] Step: ${step.name}`) - .text('='.repeat(32)) + .text(stepSection[0]) // Header with checkbox + .text(stepSection[1]) // Banner .text('') .align('lt') .size(0, 0) // Smaller size for instructions - .text(step.instructions) + .text(stepSection[3]) // Instructions .text('') .text(''); diff --git a/server/src/printer/test-printer.ts b/server/src/printer/test-printer.ts index f191197..10eba46 100644 --- a/server/src/printer/test-printer.ts +++ b/server/src/printer/test-printer.ts @@ -4,6 +4,7 @@ import { Task, Step, Printer } from '@shared/index'; import { StepRepository, PrintHistoryRepository } from '../db/repositories'; import { Knex } from 'knex'; import logger from '../logger'; +import { formatUtils } from './format-utils'; export class TestPrinter implements Printer { private readonly outputDir: string; @@ -35,15 +36,10 @@ export class TestPrinter implements Printer { const filename = path.join(this.outputDir, `task-${task.id}-${timestamp}.txt`); const content = [ - `[ ] Task: ${task.name}`, - '='.repeat(40), - '', - ...taskSteps.map((step, index) => [ - `[ ] Step ${index + 1}: ${step.name}`, - '-'.repeat(40), - step.instructions, - '', - ]).flat(), + ...formatUtils.formatSection(`Task: ${task.name}`, ''), + ...taskSteps.map((step, index) => + formatUtils.formatSection(`Step ${index + 1}: ${step.name}`, step.instructions, '-') + ).flat(), ].join('\n'); await fs.writeFile(filename, content); @@ -61,13 +57,7 @@ export class TestPrinter implements Printer { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const filename = path.join(this.outputDir, `step-${step.id}-${timestamp}.txt`); - const content = [ - `[ ] Step: ${step.name}`, - '='.repeat(40), - '', - step.instructions, - '', - ].join('\n'); + const content = formatUtils.formatSection(`Step: ${step.name}`, step.instructions).join('\n'); await fs.writeFile(filename, content); logger.info(`Printed step ${step.id} to ${filename}`);