Remove TemplateManager and related PDF rendering functionality

- Deleted the TemplateManager class and its associated methods for rendering templates and PDFs, streamlining the socket handling code.
- Removed all references to PDF and image rendering in SocketHost, SocketUser, and their tests, ensuring a cleaner codebase.
- Updated package.json and pnpm-lock.yaml to remove dependencies related to PDF generation and image processing, enhancing project simplicity.
This commit is contained in:
Tom Butcher 2026-08-19 18:18:49 +01:00
parent a3eef435f4
commit 6e74e0dfec
18 changed files with 10 additions and 2088 deletions

View File

@ -24,7 +24,6 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"ejs": "^3.1.10",
"express": "^5.1.0", "express": "^5.1.0",
"he": "^1.2.0", "he": "^1.2.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
@ -35,9 +34,6 @@
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"pdf-to-img": "^6.1.0",
"posthtml": "^0.16.7",
"puppeteer": "^24.31.0",
"redis": "^5.10.0", "redis": "^5.10.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",

804
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -52,12 +52,6 @@ jest.unstable_mockModule('../../events/eventmanager.js', () => ({
})) }))
})); }));
jest.unstable_mockModule('../../templates/templatemanager.js', () => ({
TemplateManager: jest.fn().mockImplementation(() => ({
renderPDF: jest.fn()
}))
}));
jest.unstable_mockModule('../../utils.js', () => ({ jest.unstable_mockModule('../../utils.js', () => ({
getModelByName: jest.fn(name => ({ modelName: name })) getModelByName: jest.fn(name => ({ modelName: name }))
})); }));

View File

@ -44,10 +44,6 @@ jest.unstable_mockModule('../../activity/activitymanager.js', () => ({
ActivityManager: jest.fn() ActivityManager: jest.fn()
})); }));
jest.unstable_mockModule('../../templates/templatemanager.js', () => ({
TemplateManager: jest.fn(),
}));
jest.unstable_mockModule('../../config.js', () => ({ jest.unstable_mockModule('../../config.js', () => ({
loadConfig: jest.fn(() => ({ loadConfig: jest.fn(() => ({
server: { server: {

View File

@ -116,12 +116,7 @@ describe('SocketUser', () => {
on: jest.fn(), on: jest.fn(),
emit: jest.fn() emit: jest.fn()
}; };
mockSocketManager = { mockSocketManager = {};
templateManager: {
renderTemplate: jest.fn(),
renderPDF: jest.fn()
}
};
socketUser = new SocketUser(mockSocket, mockSocketManager); socketUser = new SocketUser(mockSocket, mockSocketManager);
}); });

View File

@ -13,7 +13,6 @@ import { UpdateManager } from '../updates/updatemanager.js';
import { ActionManager } from '../actions/actionmanager.js'; import { ActionManager } from '../actions/actionmanager.js';
import { getModelByName } from '../utils.js'; import { getModelByName } from '../utils.js';
import { EventManager } from '../events/eventmanager.js'; import { EventManager } from '../events/eventmanager.js';
import { TemplateManager } from '../templates/templatemanager.js';
const config = loadConfig(); const config = loadConfig();
@ -31,7 +30,6 @@ export class SocketHost {
this.updateManager = new UpdateManager(this); this.updateManager = new UpdateManager(this);
this.actionManager = new ActionManager(this); this.actionManager = new ActionManager(this);
this.eventManager = new EventManager(this); this.eventManager = new EventManager(this);
this.templateManager = new TemplateManager(this);
this.codeAuth = new CodeAuth(); this.codeAuth = new CodeAuth();
this.setupSocketEventHandlers(); this.setupSocketEventHandlers();
} }
@ -64,14 +62,6 @@ export class SocketHost {
'unsubscribeObjectEvent', 'unsubscribeObjectEvent',
this.handleUnsubscribeObjectEventEvent.bind(this) this.handleUnsubscribeObjectEventEvent.bind(this)
); );
this.socket.on(
'renderTemplatePDF',
this.handleRenderTemplatePDFEvent.bind(this)
);
this.socket.on(
'renderTemplateJPG',
this.handleRenderTemplateJPGEvent.bind(this)
);
this.socket.on('objectEvent', this.handleObjectEventEvent.bind(this)); this.socket.on('objectEvent', this.handleObjectEventEvent.bind(this));
this.socket.on('disconnect', this.handleDisconnect.bind(this)); this.socket.on('disconnect', this.handleDisconnect.bind(this));
} }
@ -256,26 +246,6 @@ export class SocketHost {
); );
} }
async handleRenderTemplatePDFEvent(data, callback) {
const result = await this.templateManager.renderPDF(
data._id,
data.content,
data.object,
1
);
callback(result);
}
async handleRenderTemplateJPGEvent(data, callback) {
const result = await this.templateManager.renderJPG(
data._id,
data.content,
data.object,
{ width: data.width }
);
callback(result);
}
async setDevicesState(state, online, connectedAt) { async setDevicesState(state, online, connectedAt) {
logger.info('Setting devices state to', state, 'and online to', online); logger.info('Setting devices state to', state, 'and online to', online);

View File

@ -5,7 +5,6 @@ import log4js from 'log4js';
import { loadConfig } from '../config.js'; import { loadConfig } from '../config.js';
import { SocketUser } from './socketuser.js'; import { SocketUser } from './socketuser.js';
import { UpdateManager } from '../updates/updatemanager.js'; import { UpdateManager } from '../updates/updatemanager.js';
import { TemplateManager } from '../templates/templatemanager.js';
import { SocketHost } from './sockethost.js'; import { SocketHost } from './sockethost.js';
const config = loadConfig(); const config = loadConfig();
@ -17,7 +16,6 @@ export class SocketManager {
constructor(server) { constructor(server) {
this.socketUsers = new Map(); this.socketUsers = new Map();
this.socketHosts = new Map(); this.socketHosts = new Map();
this.templateManager = new TemplateManager(this);
// Use the provided HTTP server // Use the provided HTTP server
// Create Socket.IO server - CORS applies to HTTP long-polling transport // Create Socket.IO server - CORS applies to HTTP long-polling transport

View File

@ -34,7 +34,6 @@ export class SocketUser {
this.notificationManager = new NotificationManager(this); this.notificationManager = new NotificationManager(this);
this.serverManager = new ServerManager(this); this.serverManager = new ServerManager(this);
this.userSettingsManager = new UserSettingsManager(this); this.userSettingsManager = new UserSettingsManager(this);
this.templateManager = socketManager.templateManager;
this.keycloakAuth = new KeycloakAuth(); this.keycloakAuth = new KeycloakAuth();
this.setupSocketEventHandlers(); this.setupSocketEventHandlers();
} }
@ -85,18 +84,6 @@ export class SocketUser {
'unsubscribeModelStats', 'unsubscribeModelStats',
this.handleUnsubscribeToStatsEvent.bind(this) this.handleUnsubscribeToStatsEvent.bind(this)
); );
this.socket.on(
'previewTemplate',
this.handlePreviewTemplateEvent.bind(this)
);
this.socket.on(
'renderTemplatePDF',
this.handleRenderTemplatePDFEvent.bind(this)
);
this.socket.on(
'renderTemplateJPG',
this.handleRenderTemplateJPGEvent.bind(this)
);
this.socket.on( this.socket.on(
'generateHostOtp', 'generateHostOtp',
this.handleGenerateHostOtpEvent.bind(this) this.handleGenerateHostOtpEvent.bind(this)
@ -309,63 +296,6 @@ export class SocketUser {
await this.statsManager.removeStatsListener(data.objectType); await this.statsManager.removeStatsListener(data.objectType);
} }
async handlePreviewTemplateEvent(data, callback) {
try {
const result = await this.templateManager.renderTemplate(
data._id,
data.content,
data.testObject,
data.scale
);
if (typeof callback === 'function') {
callback(result);
}
} catch (err) {
logger.error('Preview template event error:', err);
if (typeof callback === 'function') {
callback({ error: err.message });
}
}
}
async handleRenderTemplatePDFEvent(data, callback) {
try {
const result = await this.templateManager.renderPDF(
data._id,
data.content,
data.object,
1
);
if (typeof callback === 'function') {
callback(result);
}
} catch (err) {
logger.error('Render template PDF event error:', err);
if (typeof callback === 'function') {
callback({ error: err.message });
}
}
}
async handleRenderTemplateJPGEvent(data, callback) {
try {
const result = await this.templateManager.renderJPG(
data._id,
data.content,
data.object,
{ width: data.width }
);
if (typeof callback === 'function') {
callback(result);
}
} catch (err) {
logger.error('Render template JPG event error:', err);
if (typeof callback === 'function') {
callback({ error: err.message });
}
}
}
async handleGenerateHostOtpEvent(data, callback) { async handleGenerateHostOtpEvent(data, callback) {
const result = await generateHostOTP(data._id); const result = await generateHostOTP(data._id);
callback(result); callback(result);

View File

@ -1,184 +0,0 @@
import { jest } from '@jest/globals';
import path from 'path';
// Mock fs before importing TemplateManager
jest.unstable_mockModule('fs', () => ({
default: {
readFileSync: jest.fn(filePath => {
if (filePath.endsWith('basetemplate.ejs'))
return '<html><%- content %></html>';
if (filePath.endsWith('styles.css')) return 'body { color: red; }';
if (filePath.endsWith('previewtemplate.ejs'))
return '<div class="preview"><%- content %></div>';
if (filePath.endsWith('rendertemplate.ejs'))
return '<div class="render"><%- content %></div>';
if (filePath.endsWith('contentplaceholder.ejs'))
return '<div class="placeholder"></div>';
if (filePath.endsWith('previewpagination.js'))
return 'console.log("pagination");';
return '';
})
}
}));
jest.unstable_mockModule('ejs', () => ({
default: {
render: jest.fn(async (content, data) => `rendered: ${content}`),
compile: jest.fn(() => jest.fn())
}
}));
jest.unstable_mockModule('posthtml', () => ({
default: jest.fn(() => ({
process: jest.fn(async content => ({ html: `transformed: ${content}` }))
}))
}));
jest.unstable_mockModule('../../database/nats.js', () => ({
natsServer: {
publish: jest.fn().mockResolvedValue({ success: true }),
subscribe: jest.fn().mockResolvedValue({ success: true }),
removeSubscription: jest.fn().mockResolvedValue({ success: true })
}
}));
jest.unstable_mockModule('../../database/redis.js', () => ({
redisServer: {
setKey: jest.fn().mockResolvedValue(undefined),
getKey: jest.fn().mockResolvedValue(null),
deleteKey: jest.fn().mockResolvedValue(undefined),
getKeysByPattern: jest.fn().mockResolvedValue([])
}
}));
jest.unstable_mockModule('../../database/database.js', () => ({
getObject: jest.fn(),
listObjects: jest.fn()
}));
jest.unstable_mockModule(
'../../database/schemas/management/documenttemplate.schema.js',
() => ({
documentTemplateModel: { modelName: 'DocumentTemplate' }
})
);
jest.unstable_mockModule('../../utils.js', () => ({
getModelByName: jest.fn(() => ({
schema: {
obj: { name: {}, status: {} }
}
}))
}));
jest.unstable_mockModule('../pdffactory.js', () => ({
generatePDF: jest.fn().mockResolvedValue(Buffer.from('pdf-data'))
}));
jest.unstable_mockModule('log4js', () => ({
default: {
getLogger: () => ({
level: 'info',
debug: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
trace: jest.fn(),
info: jest.fn()
})
}
}));
jest.unstable_mockModule('../../config.js', () => ({
loadConfig: jest.fn(() => ({
server: {
logLevel: 'info'
}
}))
}));
const { TemplateManager } = await import('../templatemanager.js');
const { getObject } = await import('../../database/database.js');
const ejs = (await import('ejs')).default;
const { generatePDF } = await import('../pdffactory.js');
describe('TemplateManager', () => {
let templateManager;
beforeEach(() => {
jest.clearAllMocks();
templateManager = new TemplateManager();
});
describe('renderTemplate', () => {
it('should render a template successfully', async () => {
const mockTemplate = {
documentSize: { width: 100, height: 100, infiniteHeight: false },
global: false,
objectType: 'printer'
};
getObject.mockResolvedValue(mockTemplate);
const result = await templateManager.renderTemplate(
'temp-id',
'some content',
{ name: 'Test' }
);
expect(getObject).toHaveBeenCalledWith(
expect.objectContaining({ id: 'temp-id' })
);
expect(ejs.render).toHaveBeenCalled();
expect(result).toHaveProperty('html');
expect(result.width).toBe(100);
expect(result.height).toBe(100);
});
it('should return error if template not found', async () => {
getObject.mockResolvedValue(null);
const result = await templateManager.renderTemplate('invalid', 'content');
expect(result).toEqual({ error: 'Document template not found.' });
});
});
describe('validateTemplate', () => {
it('should return true for valid EJS', () => {
expect(templateManager.validateTemplate('<div><%= name %></div>')).toBe(
true
);
});
it('should return false for invalid EJS', () => {
ejs.compile.mockImplementationOnce(() => {
throw new Error('syntax error');
});
expect(templateManager.validateTemplate('<% invalid %>')).toBe(false);
});
});
describe('renderPDF', () => {
it('should render a PDF successfully', async () => {
const mockTemplate = {
documentSize: { width: 100, height: 100, infiniteHeight: false },
global: false,
objectType: 'printer'
};
getObject.mockResolvedValue(mockTemplate);
const result = await templateManager.renderPDF('temp-id', 'content');
expect(generatePDF).toHaveBeenCalled();
expect(result.pdf).toBeDefined();
});
});
describe('formatDate', () => {
it('should format date correctly', () => {
const date = new Date('2023-01-01T12:00:00Z');
const formatted = templateManager.formatDate(date, 'YYYY-MM-DD');
expect(formatted).toBe('2023-01-01');
});
});
});

View File

@ -1,49 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1.0" />
<title>Document</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Figtree:ital,wght@0,300..900;1,300..900&display=swap"
rel="stylesheet"
/>
<style>
<%- baseCSS %>
</style>
<style>
body {
}
.previewWrapper {
width: <%= (scaledWidth) || '50mm' %>;
height: <%= (scaledHeight) || '50mm' %>;
}
.previewDocument {
width: <%= (width) || '50mm' %>;
height: <%= (height) || '50mm' %>;
transform: scale(<%= scale || '1' %>);
transform-origin: top left;
}
.renderDocument {
width: <%= (scaledWidth) || '50mm' %>;
height: <%= (scaledHeight) || '50mm' %>;
transform: scale(<%= scale || '1' %>);
}
</style>
</head>
<body>
<%- content %>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.0/dist/JsBarcode.all.min.js"></script>
<script>
JsBarcode('.documentBarcode').init();
</script>
<% if (typeof previewPaginationScript !== 'undefined' && previewPaginationScript) { %>
<script>
<%- previewPaginationScript %>
</script>
<% } %>
</body>
</html>

View File

@ -1,3 +0,0 @@
<div class="contentPlaceholder">
<p>Content</p>
</div>

View File

@ -1,5 +0,0 @@
<div class="previewContainer">
<div class="previewWrapper">
<div class="previewDocument" id="content"><%- content %></div>
</div>
</div>

View File

@ -1 +0,0 @@
<div class="renderDocument" id="content"><%- content %></div>

View File

@ -1,131 +0,0 @@
body {
margin: 0;
font-family: 'Figtree', sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
overflow: scroll;
}
.previewContainer {
display: flex;
justify-content: center; /* Horizontal center */
align-items: center; /* Vertical center */
padding: 60px;
box-sizing: border-box;
width: fit-content;
}
.previewWrapper {
position: relative;
}
.previewDocument {
background: #ffffff;
border: 1px solid #000;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
transform-origin: top left;
position: relative;
overflow: hidden;
}
.documentText {
margin: 0;
font-size: 12px;
line-height: 1;
}
.documentTitle {
margin: 0;
}
h1.documentTitle {
font-weight: 800;
font-size: 34px;
}
h2.documentTitle {
font-weight: 800;
}
h3.documentTitle {
font-weight: 700;
}
h4.documentTitle {
font-weight: 700;
}
.documentFlex {
display: flex;
}
.documentDivider {
background: black;
height: 1px;
margin: 4px 0;
border: none;
}
.contentPlaceholder {
border: 1px solid black;
max-height: 250px;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background: repeating-linear-gradient(
45deg,
/* Angle of the stripes */ #ccc,
/* Light grey */ #ccc 10px,
/* End of first stripe */ #eee 10px,
/* Start of next stripe (slightly lighter grey) */ #eee 20px
/* End of second stripe */
);
}
.contentPlaceholder > p {
text-transform: uppercase;
font-weight: 700;
}
.documentBarcode {
width: auto !important;
height: 100% !important;
}
.documentProgressBar {
height: 8px;
width: 260px;
border-radius: 8px;
border: 1px solid #000000;
overflow: hidden;
}
.documentProgressBarInner {
height: 100%;
background: #000;
}
.documentTable {
width: 100%;
border-collapse: collapse;
border: 1px solid #000000;
}
.documentTableRow {
border: 1px solid #000000;
}
.documentTableRow td,
.documentTableRowHeader th,
.documentTableRowFooter td {
padding: 4px;
border: 1px solid #000000;
}
.documentTableRowHeader {
background: #0000002e;
text-align: left;
border: 1px solid #000000;
}
.documentTableRowFooter {
background: #0000002e;
border: 1px solid #000000;
}

View File

@ -1,78 +0,0 @@
/**
* PDF conversion utilities loaded on demand so startup
* does not require ESM-only pdf-to-img at module load time.
*/
import { pathToFileURL } from 'node:url';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
async function importPackage(name) {
const packagePath = require.resolve(name);
return import(pathToFileURL(packagePath).href);
}
function resolveDefault(module) {
return module?.default ?? module;
}
export async function convertPDFToImage(pdfInput, options = {}) {
const [pdfToImgModule, sharpModule] = await Promise.all([
importPackage('pdf-to-img'),
importPackage('sharp')
]);
const { pdf } = pdfToImgModule;
const sharp = resolveDefault(sharpModule);
try {
const { width, height, page_numbers, scale = 2, ...pdfOnlyOptions } =
options;
const pdfOptions = {
scale,
...pdfOnlyOptions
};
const document = await pdf(pdfInput, pdfOptions);
const outputImages = [];
if (
page_numbers &&
Array.isArray(page_numbers) &&
page_numbers.length > 0
) {
for (const pageNum of page_numbers) {
let image = await document.getPage(pageNum);
if (width || height) {
const resizeOptions = {};
if (width) resizeOptions.width = width;
if (height) resizeOptions.height = height;
image = await sharp(image).resize(resizeOptions).toBuffer();
}
outputImages.push(image);
}
} else {
for await (const image of document) {
let processedImage = image;
if (width || height) {
const resizeOptions = {};
if (width) resizeOptions.width = width;
if (height) resizeOptions.height = height;
processedImage = await sharp(image)
.resize(resizeOptions)
.toBuffer();
}
outputImages.push(processedImage);
}
}
return outputImages;
} catch (error) {
console.error('Error converting PDF to image:', error);
throw error;
}
}

View File

@ -1,63 +0,0 @@
import log4js from 'log4js';
import { loadConfig } from '../config.js';
const config = loadConfig();
const logger = log4js.getLogger('PDF Factory');
logger.level = config.server.logLevel;
/**
* Generates a PDF from HTML content using Puppeteer
* @param {string} html - The HTML content to convert to PDF
* @param {Object} options - PDF generation options
* @param {number} options.width - Document width in mm
* @param {number} options.height - Document height in mm
* @returns {Promise<Buffer>} The PDF buffer
*/
export async function generatePDF(html, options = {}) {
try {
// Dynamically import puppeteer to handle cases where it might not be installed
const puppeteer = await import('puppeteer');
const browser = await puppeteer.default.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
// Set content with HTML
await page.setContent(html, {
waitUntil: 'networkidle0'
});
var height = `${options?.height || '50'}mm`;
if (options.height == 'auto') {
const calculatedHeight = await page.evaluate(() => {
return document.getElementById('content').scrollHeight;
});
height = `${calculatedHeight}px`;
}
// Generate PDF with specified dimensions
const pdfBuffer = await page.pdf({
printBackground: true,
preferCSSPageSize: true,
width: options.width ? `${options.width}mm` : undefined,
height: height ? `${height}` : undefined,
margin: {
top: '0mm',
right: '0mm',
bottom: '0mm',
left: '0mm'
}
});
await browser.close();
return pdfBuffer;
} catch (error) {
logger.error('Error generating PDF:', error.message);
throw error;
}
}

View File

@ -1,657 +0,0 @@
import ejs from 'ejs';
import log4js from 'log4js';
import posthtml from 'posthtml';
import { documentTemplateModel } from '../database/schemas/management/documenttemplate.schema.js';
import '../database/schemas/management/documentsize.schema.js';
// Load configuration
import { loadConfig } from '../config.js';
import fs from 'fs';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc.js';
import timezone from 'dayjs/plugin/timezone.js';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { getObject, listObjects } from '../database/database.js';
import { getModelByName } from '../utils.js';
import { generatePDF } from './pdffactory.js';
import { convertPDFToImage } from './pdfUtils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Extend plugins
dayjs.extend(utc);
dayjs.extend(timezone);
const config = loadConfig();
const logger = log4js.getLogger('Template Manager');
logger.level = config.server.logLevel;
let baseTemplate;
let baseCSS;
let previewTemplate;
let renderTemplateEjs;
let contentPlaceholder;
let previewPaginationScript;
async function loadTemplates() {
// Synchronously load files
baseTemplate = fs.readFileSync(
join(__dirname, '/assets/basetemplate.ejs'),
'utf8'
);
baseCSS = fs.readFileSync(join(__dirname, '/assets/styles.css'), 'utf8');
previewTemplate = fs.readFileSync(
join(__dirname, '/assets/previewtemplate.ejs'),
'utf8'
);
renderTemplateEjs = fs.readFileSync(
join(__dirname, '/assets/rendertemplate.ejs'),
'utf8'
);
contentPlaceholder = fs.readFileSync(
join(__dirname, '/assets/contentplaceholder.ejs'),
'utf8'
);
previewPaginationScript = fs.readFileSync(
join(__dirname, '/assets/previewpagination.js'),
'utf8'
);
}
loadTemplates();
function getNodeStyles(attributes) {
var styles = '';
if (attributes?.padding) {
styles += `padding: ${attributes.padding};`;
}
if (attributes?.width) {
styles += `width: ${attributes.width};`;
}
if (attributes?.height) {
styles += `height: ${attributes.height};`;
}
if (attributes?.maxWidth) {
styles += `max-width: ${attributes.maxWidth};`;
}
if (attributes?.maxHeight) {
styles += `max-height: ${attributes.maxHeight};`;
}
if (attributes?.gap && attributes?.vertical != 'true') {
styles += `column-gap: ${attributes.gap};`;
}
if (attributes?.gap && attributes?.vertical == 'true') {
styles += `row-gap: ${attributes.gap};`;
}
if (attributes?.justify) {
styles += `justify-content: ${attributes.justify};`;
}
if (attributes?.align) {
styles += `align-items: ${attributes.align};`;
}
if (attributes?.border) {
styles += `border: ${attributes.border};`;
}
if (attributes?.borderRadius) {
styles += `border-radius: ${attributes.borderRadius};`;
}
if (attributes?.vertical == 'true') {
styles += `flex-direction: column;`;
}
if (attributes?.grow) {
styles += `flex-grow: ${attributes.grow};`;
}
if (attributes?.shrink) {
styles += `flex-shrink: ${attributes.shrink};`;
}
if (attributes?.color) {
styles += `color: ${attributes.color};`;
}
if (attributes?.background) {
styles += `background: ${attributes.background};`;
}
if (attributes?.scale) {
styles += `transform: scale(${attributes.scale});`;
}
if (attributes?.textAlign) {
styles += `text-align: ${attributes.textAlign};`;
}
if (attributes?.textSize) {
styles += `font-size: ${attributes.textSize};`;
}
if (attributes?.wordWrap) {
styles += `word-wrap: ${attributes.wordWrap};`;
}
return styles;
}
async function transformCustomElements(content) {
const result = await posthtml([
tree =>
tree.match({ tag: 'Title1' }, node => ({
...node,
tag: 'h1',
attrs: { class: 'documentTitle' }
})),
tree =>
tree.match({ tag: 'Title2' }, node => ({
...node,
tag: 'h2',
attrs: { class: 'documentTitle' }
})),
tree =>
tree.match({ tag: 'Title3' }, node => ({
...node,
tag: 'h3',
attrs: { class: 'documentText' }
})),
tree =>
tree.match({ tag: 'Title4' }, node => ({
...node,
tag: 'h4',
attrs: { class: 'documentText' }
})),
tree =>
tree.match({ tag: 'Text' }, node => ({
...node,
tag: 'p',
attrs: { class: 'documentText', style: getNodeStyles(node.attrs) }
})),
tree =>
tree.match({ tag: 'Bold' }, node => ({
...node,
tag: 'strong',
attrs: {
style: 'font-weight: bold;',
class: 'documentBoldText',
style: getNodeStyles(node.attrs)
}
})),
tree =>
tree.match({ tag: 'Barcode' }, node => {
return {
tag: 'div',
content: [
{
tag: 'svg',
attrs: {
class: 'documentBarcode',
'jsbarcode-displayValue': 'false',
'jsbarcode-value': node.content.at(0) || '',
'jsbarcode-format': node.attrs.format || 'code128',
'jsbarcode-width': node.attrs.barcodeWidth || 2,
'jsbarcode-margin': 0
}
}
],
attrs: {
class: 'documentBarcodeContainer',
style: getNodeStyles(node.attrs)
}
};
}),
tree =>
tree.match({ tag: 'Container' }, node => ({
...node,
tag: 'div',
attrs: {
class: 'documentContainer',
style: getNodeStyles(node.attrs)
}
})),
tree =>
tree.match({ tag: 'Flex' }, node => {
return {
...node,
tag: 'div',
attrs: {
class: 'documentFlex',
style: getNodeStyles(node.attrs)
}
};
}),
tree =>
tree.match({ tag: 'Divider' }, node => {
return {
...node,
tag: 'hr',
attrs: {
class: 'documentDivider',
style: getNodeStyles(node.attrs)
}
};
}),
tree =>
tree.match({ tag: 'ProgressBar' }, node => {
return {
...node,
tag: 'div',
attrs: {
class: 'documentProgressBar',
style: getNodeStyles(node.attrs)
},
content: [
{
tag: 'div',
attrs: {
class: 'documentProgressBarInner',
style: `width: ${Math.round((node.content[0] || 0) * 100)}%`
}
}
]
};
}),
tree =>
tree.match({ tag: 'DateTime' }, node => {
const dateTime = dayjs.utc(node.content[0]);
return {
content: [dateTime.format('YYYY-MM-DD hh:mm:ss')],
tag: 'span',
attrs: {
class: 'documentDateTime',
style: getNodeStyles(node.attrs)
}
};
}),
tree =>
tree.match({ tag: 'Table' }, node => {
return {
...node,
tag: 'table',
attrs: {
class: 'documentTable',
style: getNodeStyles(node.attrs)
}
};
}),
tree =>
tree.match({ tag: 'Row' }, node => {
const rowType = node.attrs?.type?.toLowerCase() || '';
// Transform Col children based on the row type (header/footer/body)
const transformCols = content => {
if (!Array.isArray(content)) return content;
return content.map(child => {
if (typeof child === 'string' || child == null) {
return child;
}
if (child.tag !== 'Col') {
return child;
}
const baseAttrs = {
...child.attrs,
style: getNodeStyles(child.attrs)
};
if (rowType === 'header') {
// Header row columns become table headers
return {
...child,
tag: 'th',
attrs: baseAttrs
};
}
// Footer and body rows both use <td>; footer is distinguished by the row class
return {
...child,
tag: 'td',
attrs: baseAttrs
};
});
};
const content = transformCols(node.content);
if (rowType === 'header') {
return {
...node,
tag: 'tr',
content,
attrs: {
class: 'documentTableRowHeader',
style: getNodeStyles(node.attrs)
}
};
}
if (rowType === 'footer') {
return {
...node,
tag: 'tr',
content,
attrs: {
class: 'documentTableRowFooter',
style: getNodeStyles(node.attrs)
}
};
}
return {
...node,
tag: 'tr',
content,
attrs: {
class: 'documentTableRow',
style: getNodeStyles(node.attrs)
}
};
})
]).process(content);
return result.html;
}
export class TemplateManager {
constructor() {
this.fc = {
listObjects: this.listObjects.bind(this),
getObject: this.getObject.bind(this),
formatDate: this.formatDate.bind(this)
};
}
/**
* Previews an EJS template by rendering it with provided data
* @param {string} templateString - The EJS template as a string
* @param {Object} data - Data object to pass to the template
* @param {Object} options - EJS rendering options
* @returns {Promise<string>} The rendered HTML string
*/
async renderTemplate(
id,
content,
data = {},
scale = 1,
options = {},
preview = true
) {
try {
// Set default options for EJS rendering
const defaultOptions = {
async: true,
...options
};
logger.debug('Rendering template:', id);
const documentTemplate = await getObject({
model: documentTemplateModel,
id,
populate: [
{ path: 'documentSize' },
{ path: 'parent', strictPopulate: false }
],
cached: true
});
if (documentTemplate == null) {
return { error: 'Document template not found.' };
}
const documentSize = documentTemplate.documentSize;
if (documentSize == null) {
return { error: 'Document template size not found.' };
}
// Validate content parameter
if (content == null || typeof content !== 'string') {
return { error: 'Template content is required and must be a string.' };
}
// Make sure data has default undefefined values and then merge with data
var templateData = {};
if (documentTemplate.global == true) {
templateData = { content: contentPlaceholder, fc: this.fc };
} else {
const objectType = documentTemplate?.objectType;
const model = getModelByName(objectType);
if (model == null) {
return { error: `Unknown object type: ${objectType}` };
}
const defaultKeys = Object.keys(model.schema.obj);
const defaultValues = {};
for (const key of defaultKeys) {
defaultValues[key] = null;
}
templateData = { ...defaultValues, ...data, fc: this.fc };
}
// Render the template
const templateContent = await ejs.render(
content,
templateData,
defaultOptions
);
var templateWithParentContent;
var parentTemplate = documentTemplate.parent;
if (documentTemplate.parent != undefined) {
if (typeof parentTemplate === 'string') {
parentTemplate = await getObject({
model: documentTemplateModel,
id: parentTemplate,
populate: [
{ path: 'documentSize' },
{ path: 'parent', strictPopulate: false }
],
cached: true
});
}
// Validate parent content
if (
parentTemplate.content == null ||
typeof parentTemplate.content !== 'string'
) {
logger.error(
'Parent template content is required and must be a string.',
parentTemplate.content
);
return {
error:
'Parent template content is required and must be a string.' +
parentTemplate.content
};
}
templateWithParentContent = await ejs.render(
parentTemplate.content,
{ content: templateContent, fc: this.fc },
defaultOptions
);
} else {
templateWithParentContent = templateContent;
}
// Validate rendered content before transformation
if (
templateWithParentContent == null ||
typeof templateWithParentContent !== 'string'
) {
return { error: 'Failed to render template content.' };
}
const templateHtml = await transformCustomElements(
templateWithParentContent
);
// Validate transformed HTML
if (templateHtml == null || typeof templateHtml !== 'string') {
return { error: 'Failed to transform template content.' };
}
var innerHtml = null;
if (preview == true) {
innerHtml = await ejs.render(
previewTemplate,
{ content: templateHtml },
defaultOptions
);
} else {
innerHtml = await ejs.render(
renderTemplateEjs,
{ content: templateHtml },
defaultOptions
);
}
// Validate inner HTML
if (innerHtml == null || typeof innerHtml !== 'string') {
return { error: 'Failed to render inner template content.' };
}
const infiniteHeight = documentSize.infiniteHeight == true;
const baseHtml = await ejs.render(
baseTemplate,
{
content: innerHtml,
width: `${documentSize.width}mm`,
height: infiniteHeight ? 'fit-content' : `${documentSize.height}mm`,
scaledWidth: `${documentSize.width * scale}mm`,
scaledHeight: infiniteHeight
? 'auto'
: `${documentSize.height * scale}mm`,
scale: `${scale}`,
baseCSS: baseCSS,
previewPaginationScript: preview ? previewPaginationScript : ''
},
defaultOptions
);
const previewObject = {
html: baseHtml,
width: documentSize.width,
height: infiniteHeight ? 'auto' : documentSize.height,
infiniteHeight: infiniteHeight
};
return previewObject;
} catch (error) {
logger.warn('Error whilst previewing template:', error.message);
return { error: error.message };
}
}
/**
* Validates if a template string is valid EJS syntax
* @param {string} templateString - The EJS template as a string
* @returns {boolean} True if template is valid, false otherwise
*/
validateTemplate(templateString) {
try {
// Try to compile the template to check for syntax errors
ejs.compile(templateString);
return true;
} catch (error) {
return false;
}
}
/**
* Renders a template to PDF format
* @param {string} id - The document template ID
* @param {string} content - The template content
* @param {Object} data - Data object to pass to the template
* @param {number} scale - Scale factor for rendering
* @param {Object} options - EJS rendering options
* @returns {Promise<Object>} Object containing PDF buffer or error
*/
async renderPDF(id, content, data = {}, options = {}) {
try {
logger.debug('Rendering PDF for template:', id);
const renderedTemplate = await this.renderTemplate(
id,
content,
data,
1,
options,
false
);
if (renderedTemplate.error != undefined) {
return { error: renderedTemplate.error };
}
const baseHtml = renderedTemplate.html;
// Generate PDF using PDF factory
const pdfBuffer = await generatePDF(baseHtml, {
width: renderedTemplate.width,
height: renderedTemplate.height,
infiniteHeight: renderedTemplate.infiniteHeight
});
const pdfObject = {
pdf: pdfBuffer
};
return pdfObject;
} catch (error) {
logger.warn('Error whilst rendering PDF:', error.message);
return { error: error.message };
}
}
/**
* Renders a template to image format for receipt printers
* @param {string} id - The document template ID
* @param {string} content - The template content
* @param {Object} data - Data object to pass to the template
* @param {Object} options - Image conversion options (e.g. width, height)
* @returns {Promise<Object>} Object containing image buffers or error
*/
async renderJPG(id, content, data = {}, options = {}) {
try {
logger.debug('Rendering JPG for template:', id);
const pdfResult = await this.renderPDF(id, content, data, options);
if (pdfResult.error != undefined) {
return { error: pdfResult.error };
}
const images = await convertPDFToImage(pdfResult.pdf, options);
return { images };
} catch (error) {
logger.warn('Error whilst rendering JPG:', error.message);
return { error: error.message };
}
}
async listObjects(objectType, filter = {}, populate = []) {
const model = getModelByName(objectType);
if (model == undefined) {
throw new Error('Farm Control: Object type not found.');
}
const objects = await listObjects({
model,
filter,
populate
});
return objects;
}
formatDate(date, format) {
return dayjs(date).format(format);
}
async getObject(objectType = undefined, id = undefined, populate = []) {
if (objectType == undefined || objectType == '') {
logger.warn('Object type is required');
return { error: 'Object type is required' };
}
if (id == undefined || id == '') {
logger.warn('Object ID is required');
return { error: 'Object ID is required' };
}
const model = getModelByName(objectType);
if (model == undefined) {
logger.warn('Object type not found:', objectType);
}
const object = await getObject({ model, id, cached: true, populate });
return object;
}
}