Add Template Manager and associated assets for document rendering

- Implemented TemplateManager class for rendering EJS templates with dynamic content.
- Added base template, preview template, content placeholder, and render template EJS files.
- Introduced CSS styles for document layout and presentation.
- Integrated dayjs for date formatting and posthtml for custom element transformation.
This commit is contained in:
Tom Butcher 2025-08-18 01:05:57 +01:00
parent bf56234c4b
commit 5584e61583
6 changed files with 421 additions and 0 deletions

View File

@ -0,0 +1,45 @@
<!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 {
min-width: calc(<%= width || '50mm' %> + 100px);
min-height: calc(<%= height || '50mm' %> + 100px);
}
.previewContainer {
transform: scale(<%= scale || '1' %>);
min-width: calc(<%= width || '50mm' %> + 100px);
min-height: calc(<%= height || '50mm' %> + 100px);
}
.previewDocument {
width: <%= width || '50mm' %>;
height: <%= height || '50mm' %>;
}
.renderDocument {
width: <%= width || '50mm' %>;
height: <%= height || '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>
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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