receiptline
Version:
Markdown for receipts. Printable digital receipts. Generate receipt printer commands and images.
1,194 lines (1,152 loc) • 170 kB
JavaScript
/*
Copyright 2019 Open Foodservice System Consortium
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// QR Code is a registered trademark of DENSO WAVE INCORPORATED.
(function () {
let iconv = undefined;
let PNG = undefined;
let stream = undefined;
let decoder = undefined;
let qrcode = undefined;
// Node.js
if (typeof require !== 'undefined') {
iconv = require('iconv-lite');
PNG = require('pngjs').PNG;
stream = require('stream');
decoder = require('string_decoder');
qrcode = require('./qrcode-generator/qrcode.js');
}
/**
* Transform ReceiptLine document to printer commands or SVG images.
* @param {string} doc ReceiptLine document
* @param {object} [printer] printer configuration
* @returns {string} printer command or SVG image
*/
function transform(doc, printer) {
// web browser
qrcode = qrcode || window.qrcode;
// initialize state variables
const state = {
wrap: true,
border: 1,
width: [],
align: 1,
option: { type: 'code128', width: 2, height: 72, hri: false, cell: 3, level: 'l' },
line: 'waiting',
rules: { left: 0, width: 0, right: 0, widths: [] }
};
// validate printer configuration
const ptr = parseOption(printer);
// append commands to start printing
let result = ptr.command.open(ptr);
// strip bom
if (doc[0] === '\ufeff') {
doc = doc.slice(1);
}
// parse each line and generate commands
const res = doc.normalize().split(/\n|\r\n|\r/).map(line => createLine(parseLine(line, state), ptr, state));
// if rules is not finished
switch (state.line) {
case 'ready':
// set state to cancel rules
state.line = 'waiting';
break;
case 'running':
case 'horizontal':
// append commands to stop rules
res.push(ptr.command.normal() +
ptr.command.area(state.rules.left, state.rules.width, state.rules.right) +
ptr.command.align(0) +
ptr.command.vrstop(state.rules.widths) +
ptr.command.vrlf(false));
state.line = 'waiting';
break;
default:
break;
}
// flip upside down
if (ptr.upsideDown) {
res.reverse();
}
// append commands
result += res.join('');
// append commands to end printing
result += ptr.command.close();
return result;
}
/**
* Create transform stream that converts ReceiptLine document to printer commands or SVG images.
* @param {object} [printer] printer configuration
* @returns {stream.Transform} transform stream
*/
function createTransform(printer) {
// initialize state variables
const state = {
wrap: true,
border: 1,
width: [],
align: 1,
option: { type: 'code128', width: 2, height: 72, hri: false, cell: 3, level: 'l' },
line: 'waiting',
rules: { left: 0, width: 0, right: 0, widths: [] }
};
// validate printer configuration
const ptr = parseOption(printer);
// create transform stream
const transform = new stream.Transform({
construct(callback) {
// initialize
this.bom = true;
this.decoder = new decoder.StringDecoder('utf8');
this.data = '';
this.encoding = /^(svg|text)$/.test(printer.command) ? 'utf8' : 'binary';
this._push = function (chunk) {
if (chunk.length > 0) {
this.push(chunk, this.encoding);
}
};
this.buffer = [];
// append commands to start printing
this._push(ptr.command.open(ptr));
callback();
},
transform(chunk, encoding, callback) {
// append chunk
this.data += this.decoder.write(chunk);
// strip bom
if (this.bom) {
if (this.data[0] === '\ufeff') {
this.data = this.data.slice(1);
}
this.bom = false;
}
// parse each line and generate commands
const lines = this.data.split(/\n|\r\n|\r/);
while (lines.length > 1) {
const s = createLine(parseLine(lines.shift().normalize(), state), ptr, state);
ptr.upsideDown ? this.buffer.push(s) : this._push(s);
}
this.data = lines.shift();
callback();
},
flush(callback) {
// parse last line and generate commands
const s = createLine(parseLine(this.data.normalize(), state), ptr, state);
ptr.upsideDown ? this.buffer.push(s) : this._push(s);
// if rules is not finished
switch (state.line) {
case 'ready':
// set state to cancel rules
state.line = 'waiting';
break;
case 'running':
case 'horizontal':
// append commands to stop rules
const s = ptr.command.normal() +
ptr.command.area(state.rules.left, state.rules.width, state.rules.right) +
ptr.command.align(0) +
ptr.command.vrstop(state.rules.widths) +
ptr.command.vrlf(false);
ptr.upsideDown ? this.buffer.push(s) : this._push(s);
state.line = 'waiting';
break;
default:
break;
}
// flip upside down
if (ptr.upsideDown) {
this._push(this.buffer.reverse().join(''));
}
// append commands to end printing
this._push(ptr.command.close());
callback();
}
});
return transform;
}
/**
* Validate printer configuration.
* @param {object} printer printer configuration
* @returns {object} validated printer configuration
*/
function parseOption(printer) {
// validate printer configuration
const p = Object.assign({}, printer);
p.cpl = p.cpl || 48;
p.encoding = /^(cp(437|85[28]|86[0356]|1252|93[26]|949|950)|multilingual|shiftjis|gb18030|ksc5601|big5|tis620)$/.test(p.encoding) ? p.encoding : 'cp437';
p.upsideDown = !!p.upsideDown;
p.spacing = !!p.spacing;
p.cutting = 'cutting' in p ? !!p.cutting : true;
p.margin = p.margin || 0;
p.marginRight = p.marginRight || 0;
p.gradient = 'gradient' in p ? !!p.gradient : true;
p.gamma = p.gamma || 1.8;
p.threshold = p.threshold || 128;
p.command = Object.assign({}, (typeof p.command !== 'object' ? commands[p.command] : p.command) || commands.svg);
return p;
}
/**
* Parse lines.
* @param {string} columns line text without line breaks
* @param {object} state state variables
* @returns {object} parsed line object
*/
function parseLine(columns, state) {
// extract columns
const line = columns
// trim whitespace
.replace(/^[\t ]+|[\t ]+$/g, '')
// convert escape characters ('\\', '\{', '\|', '\}') to hexadecimal escape characters
.replace(/\\[\\{|}]/g, match => '\\x' + match.charCodeAt(1).toString(16))
// append a space if the first column does not start with '|' and is right-aligned
.replace(/^[^|]*[^\t |]\|/, ' $&')
// append a space if the last column does not end with '|' and is left-aligned
.replace(/\|[^\t |][^|]*$/, '$& ')
// remove '|' at the beginning of the first column
.replace(/^\|(.*)$/, '$1')
// remove '|' at the end of the last column
.replace(/^(.*)\|$/, '$1')
// separate text with '|'
.split('|')
// parse columns
.map((column, index, array) => {
// parsed column object
let result = {};
// trim whitespace
const element = column.replace(/^[\t ]+|[\t ]+$/g, '');
// determin alignment from whitespaces around column text
result.align = 1 + Number(/^[\t ]/.test(column)) - Number(/[\t ]$/.test(column));
// parse properties
if (/^\{[^{}]*\}$/.test(element)) {
// extract members
result.property = element
// trim property delimiters
.slice(1, -1)
// convert escape character ('\;') to hexadecimal escape characters
.replace(/\\;/g, '\\x3b')
// separate property with ';'
.split(';')
// parse members
.reduce((obj, member) => {
// abbreviations
const abbr = { a: 'align', b: 'border', c: 'code', i: 'image', o: 'option', t: 'text', w: 'width', x: 'command', _: 'comment' };
// parse key-value pair
if (!/^[\t ]*$/.test(member) &&
member.replace(/^[\t ]*([A-Za-z_]\w*)[\t ]*:[\t ]*([^\t ].*?)[\t ]*$/,
(match, key, value) => obj[key.replace(/^[abciotwx_]$/, m => abbr[m])] = parseEscape(value.replace(/\\n/g, '\n'))) === member) {
// invalid members
result.error = element;
}
return obj;
}, {});
// if the column is single
if (array.length === 1) {
// parse text property
if ('text' in result.property) {
const c = result.property.text.toLowerCase();
state.wrap = !/^nowrap$/.test(c);
}
// parse border property
if ('border' in result.property) {
const c = result.property.border.toLowerCase();
const border = { 'line': -1, 'space': 1, 'none': 0 };
const previous = state.border;
state.border = /^(line|space|none)$/.test(c) ? border[c.toLowerCase()] : /^\d+$/.test(c) && Number(c) <= 2 ? Number(c) : 1;
// start rules
if (previous >= 0 && state.border < 0) {
result.vr = '+';
}
// stop rules
if (previous < 0 && state.border >= 0) {
result.vr = '-';
}
}
// parse width property
if ('width' in result.property) {
const width = result.property.width.toLowerCase().split(/[\t ]+|,/);
state.width = width.find(c => /^auto$/.test(c)) ? [] : width.map(c => /^\*$/.test(c) ? -1 : /^\d+$/.test(c) ? Number(c) : 0);
}
// parse align property
if ('align' in result.property) {
const c = result.property.align.toLowerCase();
const align = { 'left': 0, 'center': 1, 'right': 2 };
state.align = /^(left|center|right)$/.test(c) ? align[c.toLowerCase()] : 1;
}
// parse option property
if ('option' in result.property) {
const option = result.property.option.toLowerCase().split(/[\t ]+|,/);
state.option = {
type: (option.find(c => /^(upc|ean|jan|code39|itf|codabar|nw7|code93|code128|qrcode)$/.test(c)) || 'code128'),
width: Number(option.find(c => /^\d+$/.test(c) && Number(c) >= 2 && Number(c) <= 4) || '2'),
height: Number(option.find(c => /^\d+$/.test(c) && Number(c) >= 24 && Number(c) <= 240) || '72'),
hri: !!option.find(c => /^hri$/.test(c)),
cell: Number(option.find(c => /^\d+$/.test(c) && Number(c) >= 3 && Number(c) <= 8) || '3'),
level: (option.find(c => /^[lmqh]$/.test(c)) || 'l')
};
}
// parse code property
if ('code' in result.property) {
result.code = Object.assign({ data: result.property.code }, state.option);
}
// parse image property
if ('image' in result.property) {
const c = result.property.image.replace(/=.*|[^A-Za-z0-9+/]/g, '');
switch (c.length % 4) {
case 1:
result.image = c.slice(0, -1);
break;
case 2:
result.image = c + '==';
break;
case 3:
result.image = c + '=';
break;
default:
result.image = c;
break;
}
}
// parse command property
if ('command' in result.property) {
result.command = result.property.command;
}
// parse comment property
if ('comment' in result.property) {
result.comment = result.property.comment;
}
}
}
// remove invalid property delimiter
else if (/[{}]/.test(element)) {
result.error = element;
}
// parse horizontal rule of special character in text
else if (array.length === 1 && /^-+$|^=+$/.test(element)) {
result.hr = element.slice(-1);
}
// parse text
else {
result.text = element
// remove control codes and hexadecimal control codes
.replace(/[\x00-\x1f\x7f]|\\x[01][\dA-Fa-f]|\\x7[Ff]/g, '')
// convert escape characters ('\-', '\=', '\_', '\"', \`', '\^', '\~') to hexadecimal escape characters
.replace(/\\[-=_"`^~]/g, match => '\\x' + match.charCodeAt(1).toString(16))
// convert escape character ('\n') to LF
.replace(/\\n/g, '\n')
// convert escape character ('~') to space
.replace(/~/g, ' ')
// separate text with '_', '"', '`', '^'(1 or more), '\n'
.split(/([_"`\n]|\^+)/)
// convert escape characters to normal characters
.map(text => parseEscape(text));
}
// set current text wrapping
result.wrap = state.wrap;
// set current column border
result.border = state.border;
// set current column width
if (state.width.length === 0) {
// set '*' for all columns when the width property is 'auto'
result.width = -1;
}
else if ('text' in result) {
// text: set column width
result.width = index < state.width.length ? state.width[index] : 0;
}
else if (state.width.find(c => c < 0)) {
// image, code, command: when the width property includes '*', set '*'
result.width = -1;
}
else {
// image, code, command: when the width property does not include '*', set the sum of column width and border width
const w = state.width.filter(c => c > 0);
result.width = w.length > 0 ? w.reduce((a, c) => a + c, result.border < 0 ? w.length + 1 : (w.length - 1) * result.border) : 0;
}
// set line alignment
result.alignment = state.align;
return result;
});
// if the line is text and the width property is not 'auto'
if (line.every(el => 'text' in el) && state.width.length > 0) {
// if the line has fewer columns
while (line.length < state.width.length) {
// fill empty columns
line.push({ align: 1, text: [''], wrap: state.wrap, border: state.border, width: state.width[line.length] });
}
}
return line;
}
/**
* Parse escape characters.
* @param {string} chars string containing escape characters
* @returns {string} unescaped string
*/
function parseEscape(chars) {
return chars
// remove invalid escape sequences
.replace(/\\$|\\x(.?$|[^\dA-Fa-f].|.[^\dA-Fa-f])/g, '')
// ignore invalid escape characters
.replace(/\\[^x]/g, '')
// convert hexadecimal escape characters to normal characters
.replace(/\\x([\dA-Fa-f]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16)));
}
/**
* Generate commands from line objects.
* @param {object} line parsed line object
* @param {object} printer printer configuration
* @param {object} state state variables
* @returns {string} printer command fragment or SVG image fragment
*/
function createLine(line, printer, state) {
const result = [];
// text or property
const text = line.every(el => 'text' in el);
// the first column
const column = line[0];
// remove zero width columns
let columns = line.filter(el => el.width !== 0);
// remove overflowing columns
if (text) {
columns = columns.slice(0, Math.floor(column.border < 0 ? (printer.cpl - 1) / 2 : (printer.cpl + column.border) / (column.border + 1)));
}
// fixed columns
const f = columns.filter(el => el.width > 0);
// variable columns
const g = columns.filter(el => el.width < 0);
// reserved width
let u = f.reduce((a, el) => a + el.width, 0);
// free width
let v = printer.cpl - u;
// subtract border width from free width
if (text && columns.length > 0) {
v -= column.border < 0 ? columns.length + 1 : (columns.length - 1) * column.border;
}
// number of variable columns
const n = g.length;
// reduce the width of fixed columns when reserved width is too many
while (n > v) {
f.reduce((a, el) => a.width > el.width ? a : el).width--;
v++;
}
// allocate free width among variable columns
if (n > 0) {
g.forEach((el, i) => el.width = Math.floor((v + i) / n));
v = 0;
}
// print area
const left = Math.floor(v * column.alignment / 2);
const width = printer.cpl - v;
const right = v - left;
// process text
if (text) {
// wrap text
const cols = columns.map(column => wrapText(column, printer));
// vertical line spacing
const widths = columns.map(column => column.width);
// rules
switch (state.line) {
case 'ready':
// append commands to start rules
result.push(printer.command.normal() +
printer.command.area(left, width, right) +
printer.command.align(0) +
printer.command.vrstart(widths) +
printer.command.vrlf(true));
state.line = 'running';
break;
case 'horizontal':
// append commands to print horizontal rule
const m = left - state.rules.left;
const w = width - state.rules.width;
const l = Math.min(left, state.rules.left);
const r = Math.min(right, state.rules.right);
result.push(printer.command.normal() +
printer.command.area(l, printer.cpl - l - r, r) +
printer.command.align(0) +
printer.command.vrhr(state.rules.widths, widths, m, m + w) +
printer.command.lf());
state.line = 'running';
break;
default:
break;
}
// save parameters to stop rules
state.rules = { left: left, width: width, right: right, widths: widths };
// maximum number of wraps
const row = column.wrap ? cols.reduce((a, col) => Math.max(a, col.length), 1) : 1;
// sort text
for (let j = 0; j < row; j++) {
// append commands to set print area and line alignment
let res = printer.command.normal() +
printer.command.area(left, width, right) +
printer.command.align(0);
// print position
let p = 0;
// process vertical rules
if (state.line === 'running') {
// maximum height
const height = cols.reduce((a, col) => j < col.length ? Math.max(a, col[j].height) : a, 1);
// append commands to print vertical rules
res += printer.command.normal() +
printer.command.absolute(p++) +
printer.command.vr(widths, height);
}
// process each column
cols.forEach((col, i) => {
// append commands to set print position of first column
res += printer.command.absolute(p);
// if wrapped text is not empty
if (j < col.length) {
// append commands to align text
res += printer.command.relative(col[j].margin);
// process text
const data = col[j].data;
for (let k = 0; k < data.length; k += 2) {
// append commands to decorate text
const ul = Number(data[k][0]);
const em = Number(data[k][1]);
const iv = Number(data[k][2]);
const wh = Number(data[k][3]);
res += printer.command.normal();
if (ul) {
res += printer.command.ul();
}
if (em) {
res += printer.command.em();
}
if (iv) {
res += printer.command.iv();
}
if (wh) {
res += printer.command.wh(wh);
}
// append commands to print text
res += printer.command.text(data[k + 1], printer.encoding);
}
}
// if wrapped text is empty
else {
res += printer.command.normal() + printer.command.text(' ', printer.encoding);
}
// append commands to set print position of next column
p += columns[i].width + Math.abs(column.border);
});
// append commands to feed new line
res += printer.command.lf();
result.push(res);
}
}
// process horizontal rule or paper cut
if ('hr' in column) {
// process paper cut
if (column.hr === '=') {
switch (state.line) {
case 'running':
case 'horizontal':
// append commands to stop rules
result.push(printer.command.normal() +
printer.command.area(state.rules.left, state.rules.width, state.rules.right) +
printer.command.align(0) +
printer.command.vrstop(state.rules.widths) +
printer.command.vrlf(false));
// append commands to cut paper
result.push(printer.command.cut());
// set state to start rules
state.line = 'ready';
break;
default:
// append commands to cut paper
result.push(printer.command.cut());
break;
}
}
// process horizontal rule
else {
switch (state.line) {
case 'waiting':
// append commands to print horizontal rule
result.push(printer.command.normal() +
printer.command.area(left, width, right) +
printer.command.align(0) +
printer.command.hr(width) +
printer.command.lf());
break;
case 'running':
// set state to print horizontal rule
state.line = 'horizontal';
break;
default:
break;
}
}
}
// process rules
if ('vr' in column) {
// start rules
if (column.vr === '+') {
state.line = 'ready';
}
// stop rules
else {
switch (state.line) {
case 'ready':
// set state to cancel rules
state.line = 'waiting';
break;
case 'running':
case 'horizontal':
// append commands to stop rules
result.push(printer.command.normal() +
printer.command.area(state.rules.left, state.rules.width, state.rules.right) +
printer.command.align(0) +
printer.command.vrstop(state.rules.widths) +
printer.command.vrlf(false));
state.line = 'waiting';
break;
default:
break;
}
}
}
// process image
if ('image' in column) {
// append commands to print image
result.push(printer.command.normal() +
printer.command.area(left, width, right) +
printer.command.align(column.align) +
printer.command.image(column.image));
}
// process barcode or 2D code
if ('code' in column) {
// process 2D code
if (column.code.type === 'qrcode') {
// append commands to print 2D code
result.push(printer.command.normal() +
printer.command.area(left, width, right) +
printer.command.align(column.align) +
printer.command.qrcode(column.code, printer.encoding));
}
// process barcode
else {
// append commands to print barcode
result.push(printer.command.normal() +
printer.command.area(left, width, right) +
printer.command.align(column.align) +
printer.command.barcode(column.code, printer.encoding));
}
}
// process command
if ('command' in column) {
// append commands to insert commands
result.push(printer.command.normal() +
printer.command.area(left, width, right) +
printer.command.align(column.align) +
printer.command.command(column.command));
}
// flip upside down
if (printer.upsideDown) {
result.reverse();
}
return result.join('');
}
/**
* Wrap text.
* @param {object} column parsed column object
* @param {object} printer printer configuration
* @returns {object[]} wrapped text, text position, and text height
*/
function wrapText(column, printer) {
const result = [];
// remaining spaces
let space = column.width;
// text height
let height = 1;
// text data
let res = [];
// text decoration flags
let ul = false;
let em = false;
let iv = false;
let wh = 0;
// process text and text decoration
column.text.forEach((text, i) => {
// process text
if (i % 2 === 0) {
// if text is not empty
let t = printer.command.arrayFrom(text, printer.encoding);
while (t.length > 0) {
// measure character width
let w = 0;
let j = 0;
while (j < t.length) {
w = printer.command.measureText(t[j], printer.encoding) * (wh < 2 ? wh + 1 : wh - 1);
// output before protruding
if (w > space) {
break;
}
space -= w;
w = 0;
j++;
}
// if characters fit
if (j > 0) {
// append text decoration information
res.push((ul ? '1' : '0') + (em ? '1' : '0') + (iv ? '1' : '0') + wh);
// append text
res.push(t.slice(0, j).join(''));
// update text height
height = Math.max(height, wh < 3 ? wh : wh - 1);
// remaining text
t = t.slice(j);
}
// if character is too big
if (w > column.width) {
// do not output
t = t.slice(1);
continue;
}
// if there is no spece left
if (w > space || space === 0) {
// wrap text automatically
result.push({ data: res, margin: space * column.align / 2, height: height });
space = column.width;
res = [];
height = 1;
}
}
}
// process text decoration
else {
// update text decoration flags
switch (text) {
case '\n':
// wrap text manually
result.push({ data: res, margin: space * column.align / 2, height: height });
space = column.width;
res = [];
height = 1;
break;
case '_':
ul = !ul;
break;
case '"':
em = !em;
break;
case '`':
iv = !iv;
break;
default:
const d = Math.min(text.length, 7);
wh = wh === d ? 0 : d;
break;
}
}
});
// output last text
if (res.length > 0) {
result.push({ data: res, margin: space * column.align / 2, height: height });
}
return result;
}
// shortcut
const $ = String.fromCharCode;
//
// Command base object
//
const _base = {
/**
* Character width.
* @type {number} character width (dots per character)
*/
charWidth: 12,
/**
* Measure text width.
* @param {string} text string to measure
* @param {string} encoding codepage
* @returns {number} string width
*/
measureText: (text, encoding) => {
let r = 0;
const t = Array.from(text);
switch (encoding) {
case 'cp932':
case 'shiftjis':
r = t.reduce((a, c) => {
const d = c.codePointAt(0);
return a + (d < 0x80 || d === 0xa5 || d === 0x203e || (d > 0xff60 && d < 0xffa0) ? 1 : 2);
}, 0);
break;
case 'cp936':
case 'gb18030':
case 'cp949':
case 'ksc5601':
case 'cp950':
case 'big5':
r = t.reduce((a, c) => a + (c.codePointAt(0) < 0x80 ? 1 : 2), 0);
break;
case 'tis620':
const a = t.reduce((a, c) => {
const d = c.codePointAt(0);
if (a.consonant) {
if (d === 0xe31 || d >= 0xe34 && d <= 0xe3a || d === 0xe47) {
if (a.vowel) {
a.length += 2;
a.consonant = a.vowel = a.tone = false;
}
else {
a.vowel = true;
}
}
else if (d >= 0xe48 && d <= 0xe4b) {
if (a.tone) {
a.length += 2;
a.consonant = a.vowel = a.tone = false;
}
else {
a.tone = true;
}
}
else if (d === 0xe33 || d >= 0xe4c && d <= 0xe4e) {
if (a.vowel || a.tone) {
a.length += 2;
a.consonant = a.vowel = a.tone = false;
}
else {
a.length += d === 0xe33 ? 2 : 1;
a.consonant = false;
}
}
else if (d >= 0xe01 && d <= 0xe2e) {
a.length++;
a.vowel = a.tone = false;
}
else {
a.length += 2;
a.consonant = a.vowel = a.tone = false;
}
}
else if (d >= 0xe01 && d <= 0xe2e) {
a.consonant = true;
}
else {
a.length++;
}
return a;
}, { length: 0, consonant: false, vowel: false, tone: false });
if (a.consonant) {
a.length++;
a.consonant = a.vowel = a.tone = false;
}
r = a.length;
break;
default:
r = t.length;
break;
}
return r;
},
/**
* Create character array from string (supporting Thai combining characters).
* @param {string} text string
* @param {string} encoding codepage
* @returns {string[]} array instance
*/
arrayFrom: (text, encoding) => {
const t = Array.from(text);
if (encoding === 'tis620') {
const a = t.reduce((a, c) => {
const d = c.codePointAt(0);
if (a.consonant) {
if (d === 0xe31 || d >= 0xe34 && d <= 0xe3a || d === 0xe47) {
if (a.vowel) {
a.result.push(a.consonant + a.vowel + a.tone, c);
a.consonant = a.vowel = a.tone = '';
}
else {
a.vowel = c;
}
}
else if (d >= 0xe48 && d <= 0xe4b) {
if (a.tone) {
a.result.push(a.consonant + a.vowel + a.tone, c);
a.consonant = a.vowel = a.tone = '';
}
else {
a.tone = c;
}
}
else if (d === 0xe33 || d >= 0xe4c && d <= 0xe4e) {
if (a.vowel || a.tone) {
a.result.push(a.consonant + a.vowel + a.tone, c);
a.consonant = a.vowel = a.tone = '';
}
else {
a.result.push(a.consonant + c);
a.consonant = '';
}
}
else if (d >= 0xe01 && d <= 0xe2e) {
a.result.push(a.consonant + a.vowel + a.tone);
a.consonant = c;
a.vowel = a.tone = '';
}
else {
a.result.push(a.consonant + a.vowel + a.tone, c);
a.consonant = a.vowel = a.tone = '';
}
}
else if (d >= 0xe01 && d <= 0xe2e) {
a.consonant = c;
}
else {
a.result.push(c);
}
return a;
}, { result: [], consonant: '', vowel: '', tone: '' });
if (a.consonant) {
a.result.push(a.consonant + a.vowel + a.tone);
a.consonant = a.vowel = a.tone = '';
}
return a.result;
}
else {
return t;
}
},
/**
* Start printing.
* @param {object} printer printer configuration
* @returns {string} commands
*/
open: printer => '',
/**
* Finish printing.
* @returns {string} commands
*/
close: () => '',
/**
* Set print area.
* @param {number} left left margin (unit: characters)
* @param {number} width print area (unit: characters)
* @param {number} right right margin (unit: characters)
* @returns {string} commands
*/
area: (left, width, right) => '',
/**
* Set line alignment.
* @param {number} align line alignment (0: left, 1: center, 2: right)
* @returns {string} commands
*/
align: align => '',
/**
* Set absolute print position.
* @param {number} position absolute position (unit: characters)
* @returns {string} commands
*/
absolute: position => '',
/**
* Set relative print position.
* @param {number} position relative position (unit: characters)
* @returns {string} commands
*/
relative: position => '',
/**
* Print horizontal rule.
* @param {number} width line width (unit: characters)
* @returns {string} commands
*/
hr: width => '',
/**
* Print vertical rules.
* @param {number[]} widths vertical line spacing
* @param {number} height text height (1-6)
* @returns {string} commands
*/
vr: (widths, height) => '',
/**
* Start rules.
* @param {number[]} widths vertical line spacing
* @returns {string} commands
*/
vrstart: widths => '',
/**
* Stop rules.
* @param {number[]} widths vertical line spacing
* @returns {string} commands
*/
vrstop: widths => '',
/**
* Print vertical and horizontal rules.
* @param {number[]} widths1 vertical line spacing (stop)
* @param {number[]} widths2 vertical line spacing (start)
* @param {number} dl difference in left position
* @param {number} dr difference in right position
* @returns {string} commands
*/
vrhr: (widths1, widths2, dl, dr) => '',
/**
* Set line spacing and feed new line.
* @param {boolean} vr whether vertical ruled lines are printed
* @returns {string} commands
*/
vrlf: vr => '',
/**
* Cut paper.
* @returns {string} commands
*/
cut: () => '',
/**
* Underline text.
* @returns {string} commands
*/
ul: () => '',
/**
* Emphasize text.
* @returns {string} commands
*/
em: () => '',
/**
* Invert text.
* @returns {string} commands
*/
iv: () => '',
/**
* Scale up text.
* @param {number} wh number of special character '^' (1-7)
* @returns {string} commands
*/
wh: wh => '',
/**
* Cancel text decoration.
* @returns {string} commands
*/
normal: () => '',
/**
* Print text.
* @param {string} text string to print
* @param {string} encoding codepage
* @returns {string} commands
*/
text: (text, encoding) => '',
/**
* Feed new line.
* @returns {string} commands
*/
lf: () => '',
/**
* Insert commands.
* @param {string} command commands to insert
* @returns {string} commands
*/
command: command => '',
/**
* Print image.
* @param {string} image image data (base64 png format)
* @returns {string} commands
*/
image: image => '',
/**
* Print QR Code.
* @param {object} symbol QR Code information (data, type, cell, level)
* @param {string} encoding codepage
* @returns {string} commands
*/
qrcode: (symbol, encoding) => '',
/**
* Print barcode.
* @param {object} symbol barcode information (data, type, width, height, hri)
* @param {string} encoding codepage
* @returns {string} commands
*/
barcode: (symbol, encoding) => ''
};
//
// SVG
//
const _svg = {
svgWidth: 576,
svgHeight: 0,
svgContent: '',
lineMargin: 0,
lineAlign: 0,
lineWidth: 48,
lineHeight: 1,
textElement: '',
textAttributes: {},
textPosition: 0,
textScale: 1,
textEncoding: '',
feedMinimum: 24,
// printer configuration
spacing: false,
// start printing:
open: function (printer) {
this.svgWidth = printer.cpl * this.charWidth;
this.svgHeight = 0;
this.svgContent = '';
this.lineMargin = 0;
this.lineAlign = 0;
this.lineWidth = printer.cpl;
this.lineHeight = 1;
this.textElement = '';
this.textAttributes = {};
this.textPosition = 0;
this.textScale = 1;
this.textEncoding = printer.encoding;
this.feedMinimum = Number(this.charWidth * (printer.spacing ? 2.5 : 2));
this.spacing = printer.spacing;
return '';
},
// finish printing:
close: function () {
const p = { font: 'monospace', size: this.charWidth * 2, style: '', lang: '' };
switch (this.textEncoding) {
case 'cp932':
case 'shiftjis':
p.font = `'Kosugi Maru', 'MS Gothic', 'San Francisco', 'Osaka-Mono', monospace`;
p.style = '@import url("https://fonts.googleapis.com/css2?family=Kosugi+Maru&display=swap");';
p.lang = 'ja';
break;
case 'cp936':
case 'gb18030':
p.size -= 2;
p.lang = 'zh-Hans';
break;
case 'cp949':
case 'ksc5601':
p.size -= 2;
p.lang = 'ko';
break;
case 'cp950':
case 'big5':
p.size -= 2;
p.lang = 'zh-Hant';
break;
case 'tis620':
p.font = `'Sarabun', monospace`;
p.size -= 4;
p.style = '@import url("https://fonts.googleapis.com/css2?family=Sarabun&display=swap");';
p.lang = 'th';
break;
default:
p.font = `'Courier Prime', 'Courier New', 'Courier', monospace`;
p.size -= 2;
p.style = '@import url("https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap");';
break;
}
if (p.style.length > 0) {
p.style = `<style type="text/css"><![CDATA[${p.style}]]></style>`;
}
if (p.lang.length > 0) {
p.lang = ` xml:lang="${p.lang}"`;
}
return `<svg width="${this.svgWidth}px" height="${this.svgHeight}px"