diff --git a/src/templates/assets/basetemplate.ejs b/src/templates/assets/basetemplate.ejs new file mode 100644 index 0000000..3b47e0b --- /dev/null +++ b/src/templates/assets/basetemplate.ejs @@ -0,0 +1,45 @@ + + + + + + Document + + + + + + + + <%- content %> + + + + diff --git a/src/templates/assets/contentplaceholder.ejs b/src/templates/assets/contentplaceholder.ejs new file mode 100644 index 0000000..6f65d2e --- /dev/null +++ b/src/templates/assets/contentplaceholder.ejs @@ -0,0 +1,3 @@ +
+

Content

+
diff --git a/src/templates/assets/previewtemplate.ejs b/src/templates/assets/previewtemplate.ejs new file mode 100644 index 0000000..ee8ff82 --- /dev/null +++ b/src/templates/assets/previewtemplate.ejs @@ -0,0 +1,3 @@ +
+
<%- content %>
+
diff --git a/src/templates/assets/rendertemplate.ejs b/src/templates/assets/rendertemplate.ejs new file mode 100644 index 0000000..8921d1f --- /dev/null +++ b/src/templates/assets/rendertemplate.ejs @@ -0,0 +1 @@ +
<%- content %>
diff --git a/src/templates/assets/styles.css b/src/templates/assets/styles.css new file mode 100644 index 0000000..d92d946 --- /dev/null +++ b/src/templates/assets/styles.css @@ -0,0 +1,73 @@ +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 */ +} +.previewDocument { + background: #ffffff; + border: 1px solid #000; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.2); +} +.documentText { + margin: 0; +} +.documentTitle { + margin: 0; +} + +h1.documentTitle { + font-weight: 800; + font-size: 38px; +} + +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; +} diff --git a/src/templates/templatemanager.js b/src/templates/templatemanager.js new file mode 100644 index 0000000..8d4fbdc --- /dev/null +++ b/src/templates/templatemanager.js @@ -0,0 +1,296 @@ +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 } from '../database/database.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 contentPlaceholder; + +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' + ); + contentPlaceholder = fs.readFileSync( + join(__dirname, '/assets/contentplaceholder.ejs'), + '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?.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};`; + } + 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' } + })), + tree => + tree.match({ tag: 'Bold' }, node => ({ + ...node, + tag: 'strong', + attrs: { style: 'font-weight: bold;', class: 'documentBoldText' } + })), + tree => + tree.match({ tag: 'Barcode' }, node => { + return { + tag: 'svg', + attrs: { + class: 'documentBarcode', + 'jsbarcode-width': node.attrs?.width, + 'jsbarcode-height': node.attrs?.height, + 'jsbarcode-value': node.content[0], + 'jsbarcode-format': node.attrs.format + } + }; + }), + 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: '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) + } + }; + }) + ]).process(content); + + return result.html; +} + +export class TemplateManager { + /** + * 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, options = {}) { + try { + // Set default options for EJS rendering + const defaultOptions = { + async: true, + ...options + }; + + 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; + + var templateData = data; + + if (documentTemplate.global == true) { + templateData = { content: contentPlaceholder }; + } + + // Render the template + const templateContent = await ejs.render( + content, + templateData, + defaultOptions + ); + + var templateWithParentContent; + + if (documentTemplate.parent != undefined) { + templateWithParentContent = await ejs.render( + documentTemplate.parent.content, + { content: templateContent }, + defaultOptions + ); + } else { + templateWithParentContent = templateContent; + } + + const templateHtml = await transformCustomElements( + templateWithParentContent + ); + + const previewHtml = await ejs.render( + previewTemplate, + { content: templateHtml }, + defaultOptions + ); + + const baseHtml = await ejs.render( + baseTemplate, + { + content: previewHtml, + width: `${documentSize.width}mm`, + height: `${documentSize.height}mm`, + scale: `${scale}`, + baseCSS: baseCSS + }, + defaultOptions + ); + + const previewObject = { + html: baseHtml + }; + + 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; + } + } +}