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'; 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[0], 'jsbarcode-format': node.attrs.format, 'jsbarcode-width': node.attrs.barcodeWidth, '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 ; 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} 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); 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; if (documentTemplate.parent != undefined) { // Validate parent content if ( documentTemplate.parent.content == null || typeof documentTemplate.parent.content !== 'string' ) { return { error: 'Parent template content is required and must be a string.' }; } templateWithParentContent = await ejs.render( documentTemplate.parent.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 baseHtml = await ejs.render( baseTemplate, { content: innerHtml, width: documentSize.width, height: documentSize.height, scale: `${scale}`, baseCSS: baseCSS, previewPaginationScript: preview ? previewPaginationScript : '' }, defaultOptions ); const previewObject = { html: baseHtml, width: documentSize.width, height: documentSize.height }; 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 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 }); const pdfObject = { pdf: pdfBuffer }; return pdfObject; } catch (error) { logger.warn('Error whilst rendering PDF:', 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, id) { const model = getModelByName(objectType); if (model == undefined) { throw new Error('Farm Control: Object type not found.'); } const object = await getObject({ model, id, cached: true }); return object; } }