// Server-side minimal ePOS-Print XML builder for Epson printers // Focused on printer-only tags. Builds the inner message for . // Tipos para las operaciones export interface Operation { op: string [key: string]: any } export interface TextStyleOptions { reverse?: boolean ul?: boolean em?: boolean color?: string } export interface PulseOptions { drawer?: string time?: string } export interface BarcodeOptions { type?: string hri?: string font?: string width?: number height?: number } export interface QRCodeOptions { model?: string level?: string size?: number } function escapeXml(str: string | number): string { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } export class EposMessageBuilder { private parts: string[] = [] // Text and styles text(content: string): this { this.parts.push(`${escapeXml(content)}`) return this } textLang(lang: string): this { this.parts.push(``) return this } textAlign(align: string): this { this.parts.push(``) return this } textRotate(rotate: boolean): this { this.parts.push(``) return this } textLineSpace(linespc: number): this { this.parts.push(``) return this } textFont(font: string): this { this.parts.push(``) return this } textSmooth(smooth: boolean): this { this.parts.push(``) return this } textDouble(dw?: boolean, dh?: boolean): this { const attrs = [ dw !== undefined ? `dw="${dw ? 'true' : 'false'}"` : '', dh !== undefined ? `dh="${dh ? 'true' : 'false'}"` : '' ].filter(Boolean).join(' ') this.parts.push(``.trim()) return this } textSize(width?: number, height?: number): this { const attrs = [ width !== undefined ? `width="${width}"` : '', height !== undefined ? `height="${height}"` : '' ].filter(Boolean).join(' ') this.parts.push(``.trim()) return this } textStyle(options: TextStyleOptions = {}): this { const { reverse, ul, em, color } = options const attrs: string[] = [] if (reverse !== undefined) attrs.push(`reverse="${reverse ? 'true' : 'false'}"`) if (ul !== undefined) attrs.push(`ul="${ul ? 'true' : 'false'}"`) if (em !== undefined) attrs.push(`em="${em ? 'true' : 'false'}"`) if (color !== undefined) attrs.push(`color="${color}"`) this.parts.push(``.trim()) return this } textPosition(x: number): this { this.parts.push(``) return this } textVPosition(y: number): this { this.parts.push(``) return this } // Feed feed(): this { this.parts.push('') return this } feedUnit(unit: number): this { this.parts.push(``) return this } feedLine(line: number): this { this.parts.push(``) return this } feedPosition(pos: number): this { this.parts.push(``) return this } // Cut cut(type: string): this { this.parts.push(``) return this } // Drawer pulse(options: PulseOptions = {}): this { const { drawer = 'drawer_1', time = 'pulse_200' } = options this.parts.push(``) return this } // Barcode and symbols barcode(data: string, options: BarcodeOptions = {}): this { const { type, hri, font, width, height } = options const attrs: string[] = [] if (type) attrs.push(`type="${type}"`) if (hri) attrs.push(`hri="${hri}"`) if (font) attrs.push(`font="${font}"`) if (width) attrs.push(`width="${width}"`) if (height) attrs.push(`height="${height}"`) this.parts.push(`${escapeXml(data)}`.trim()) return this } qrcode(data: string, options: QRCodeOptions = {}): this { const { model = 'qrcode_model_2', level = 'level_m', size } = options const attrs = [`type="${model}"`, `level="${level}"`] if (size) attrs.push(`size="${size}"`) this.parts.push(`${escapeXml(data)}`) return this } // Image imageRaw(options: { width: number height: number mode?: string base64: string }): this { const { width, height, mode = 'mono', base64 } = options this.parts.push(`${base64}`) return this } build(): string { return this.parts.join('') } } // Accepts a list of operations: [{ op: 'text', args: {...} | [] }, ...] export function buildFromOperations(ops: Operation[]): string { const b = new EposMessageBuilder() for (const item of ops || []) { const { op } = item if (!op) continue switch (op) { case 'text': b.text(item.value ?? '') break case 'textLang': b.textLang(item.lang) break case 'textAlign': b.textAlign(item.align) break case 'textRotate': b.textRotate(!!item.rotate) break case 'textLineSpace': b.textLineSpace(item.linespc) break case 'textFont': b.textFont(item.font) break case 'textSmooth': b.textSmooth(!!item.smooth) break case 'textDouble': b.textDouble(item.dw, item.dh) break case 'textSize': b.textSize(item.width, item.height) break case 'textStyle': b.textStyle({ reverse: item.reverse, ul: item.ul, em: item.em, color: item.color }) break case 'textPosition': b.textPosition(item.x) break case 'textVPosition': b.textVPosition(item.y) break case 'feed': b.feed() break case 'feedUnit': b.feedUnit(item.unit) break case 'feedLine': b.feedLine(item.line) break case 'feedPosition': b.feedPosition(item.pos) break case 'cut': b.cut(item.type || 'feed') break case 'pulse': b.pulse({ drawer: item.drawer, time: item.time }) break case 'barcode': b.barcode(item.data || '', { type: item.type, hri: item.hri, font: item.font, width: item.width, height: item.height }) break case 'qrcode': b.qrcode(item.data || '', { model: item.model, level: item.level, size: item.size }) break default: // ignore unknown for now break } } return b.build() }