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 @@
+
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 @@
+
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;
+ }
+ }
+}