farmcontrol-ws/src/templates/templatemanager.js

594 lines
16 KiB
JavaScript

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 <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);
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>} 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;
}
}