Compare commits
2 Commits
e8c9a307ab
...
bec4c8bf4c
| Author | SHA1 | Date | |
|---|---|---|---|
| bec4c8bf4c | |||
| ff60d6cf09 |
@ -51,6 +51,14 @@ export const hasPermission = async (user, objectType, action) => {
|
|||||||
|
|
||||||
export const checkPermissions = (objectType, action) => async (req, res, next) => {
|
export const checkPermissions = (objectType, action) => async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
if (req.user?._objectType == 'host') {
|
||||||
|
if (
|
||||||
|
(objectType === 'documentTemplate' && action === 'design') ||
|
||||||
|
(objectType === 'file' && action === 'download')
|
||||||
|
) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
}
|
||||||
const allowed = await hasPermission(req.user, objectType, action);
|
const allowed = await hasPermission(req.user, objectType, action);
|
||||||
if (!allowed) {
|
if (!allowed) {
|
||||||
return res.status(403).json({ error: 'Forbidden', code: 'FORBIDDEN' });
|
return res.status(403).json({ error: 'Forbidden', code: 'FORBIDDEN' });
|
||||||
|
|||||||
@ -25,6 +25,31 @@ const documentSizeSchema = new Schema(
|
|||||||
required: true,
|
required: true,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
printPadding: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
paddingLeft: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
paddingRight: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
paddingTop: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
paddingBottom: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ timestamps: true }
|
{ timestamps: true }
|
||||||
);
|
);
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
try {
|
try {
|
||||||
const session = await getSession(token);
|
const session = await getSession(token);
|
||||||
if (session && session.expiresAt > Date.now()) {
|
if (session && session.expiresAt > Date.now()) {
|
||||||
req.user = session.user;
|
req.user = { ...session.user, _objectType: 'user' };
|
||||||
req.session = session;
|
req.session = session;
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
// Try email-render JWT (short-lived token for Puppeteer email notifications)
|
// Try email-render JWT (short-lived token for Puppeteer email notifications)
|
||||||
const user = await lookupUserByToken(token);
|
const user = await lookupUserByToken(token);
|
||||||
if (user) {
|
if (user) {
|
||||||
req.user = user;
|
req.user = { ...user, _objectType: 'user' };
|
||||||
req.session = { user };
|
req.session = { user };
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
@ -85,6 +85,7 @@ const isAuthenticated = async (req, res, next) => {
|
|||||||
if (hostId && authCode) {
|
if (hostId && authCode) {
|
||||||
const host = await getObject({ model: hostModel, id: hostId });
|
const host = await getObject({ model: hostModel, id: hostId });
|
||||||
if (host && host.authCode === authCode) {
|
if (host && host.authCode === authCode) {
|
||||||
|
req.user = { ...host, _objectType: 'host' };
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,32 @@ import { getFilter, convertPropertiesString, getSort } from '../../utils.js';
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const listAllowedFilters = ['name', 'width', 'height', 'createdAt', 'updatedAt', '_reference'];
|
const listAllowedFilters = [
|
||||||
const listAllowedSorters = ['name', 'width', 'height', 'infiniteHeight', 'createdAt', 'updatedAt'];
|
'name',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'printPadding',
|
||||||
|
'paddingLeft',
|
||||||
|
'paddingRight',
|
||||||
|
'paddingTop',
|
||||||
|
'paddingBottom',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'_reference',
|
||||||
|
];
|
||||||
|
const listAllowedSorters = [
|
||||||
|
'name',
|
||||||
|
'width',
|
||||||
|
'height',
|
||||||
|
'infiniteHeight',
|
||||||
|
'printPadding',
|
||||||
|
'paddingLeft',
|
||||||
|
'paddingRight',
|
||||||
|
'paddingTop',
|
||||||
|
'paddingBottom',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
];
|
||||||
const propertiesAllowedFilters = [];
|
const propertiesAllowedFilters = [];
|
||||||
import {
|
import {
|
||||||
listDocumentSizesRouteHandler,
|
listDocumentSizesRouteHandler,
|
||||||
|
|||||||
@ -77,13 +77,34 @@ describe('Document Size Service Route Handlers', () => {
|
|||||||
|
|
||||||
describe('newDocumentSizeRouteHandler', () => {
|
describe('newDocumentSizeRouteHandler', () => {
|
||||||
it('should create a new document size', async () => {
|
it('should create a new document size', async () => {
|
||||||
req.body = { name: 'Letter', width: 216, height: 279 };
|
req.body = {
|
||||||
|
name: 'Letter',
|
||||||
|
width: 216,
|
||||||
|
height: 279,
|
||||||
|
infiniteHeight: false,
|
||||||
|
printPadding: false,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
};
|
||||||
const mockSize = { _id: '456', ...req.body };
|
const mockSize = { _id: '456', ...req.body };
|
||||||
newObject.mockResolvedValue(mockSize);
|
newObject.mockResolvedValue(mockSize);
|
||||||
|
|
||||||
await newDocumentSizeRouteHandler(req, res);
|
await newDocumentSizeRouteHandler(req, res);
|
||||||
|
|
||||||
expect(newObject).toHaveBeenCalled();
|
expect(newObject).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
newData: expect.objectContaining({
|
||||||
|
name: 'Letter',
|
||||||
|
printPadding: false,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
);
|
||||||
expect(res.send).toHaveBeenCalledWith(mockSize);
|
expect(res.send).toHaveBeenCalledWith(mockSize);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -113,7 +113,7 @@ describe('Document Template Service Route Handlers', () => {
|
|||||||
'<Text>Hello</Text>',
|
'<Text>Hello</Text>',
|
||||||
{ name: 'Test' },
|
{ name: 'Test' },
|
||||||
1,
|
1,
|
||||||
{},
|
{ padding: true },
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
expect(res.send).toHaveBeenCalledWith(mockPreview);
|
expect(res.send).toHaveBeenCalledWith(mockPreview);
|
||||||
@ -155,12 +155,34 @@ describe('Document Template Service Route Handlers', () => {
|
|||||||
'<Text>Hello</Text>',
|
'<Text>Hello</Text>',
|
||||||
{ name: 'Test' },
|
{ name: 'Test' },
|
||||||
'pdf',
|
'pdf',
|
||||||
{}
|
{ padding: true }
|
||||||
);
|
);
|
||||||
expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/pdf');
|
expect(res.set).toHaveBeenCalledWith('Content-Type', 'application/pdf');
|
||||||
expect(res.send).toHaveBeenCalledWith(pdf);
|
expect(res.send).toHaveBeenCalledWith(pdf);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('passes padding false when requested by the caller', async () => {
|
||||||
|
req.params.id = 'template-1';
|
||||||
|
req.query = { type: 'pdf', padding: 'false' };
|
||||||
|
req.body = { object: { name: 'Test' } };
|
||||||
|
templateManager.renderDownload.mockResolvedValue({
|
||||||
|
type: 'pdf',
|
||||||
|
mime: 'application/pdf',
|
||||||
|
extension: 'pdf',
|
||||||
|
buffers: [Buffer.from('pdf-data')],
|
||||||
|
});
|
||||||
|
|
||||||
|
await downloadDocumentTemplateRouteHandler(req, res);
|
||||||
|
|
||||||
|
expect(templateManager.renderDownload).toHaveBeenCalledWith(
|
||||||
|
'template-1',
|
||||||
|
undefined,
|
||||||
|
{ name: 'Test' },
|
||||||
|
'pdf',
|
||||||
|
{ padding: false }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return base64 images when multiple pages are rendered', async () => {
|
it('should return base64 images when multiple pages are rendered', async () => {
|
||||||
req.params.id = 'template-1';
|
req.params.id = 'template-1';
|
||||||
req.query = { type: 'png' };
|
req.query = { type: 'png' };
|
||||||
|
|||||||
@ -116,6 +116,11 @@ export const editDocumentSizeRouteHandler = async (req, res) => {
|
|||||||
width: req.body.width,
|
width: req.body.width,
|
||||||
height: req.body.height,
|
height: req.body.height,
|
||||||
infiniteHeight: req.body.infiniteHeight,
|
infiniteHeight: req.body.infiniteHeight,
|
||||||
|
printPadding: req.body.printPadding,
|
||||||
|
paddingLeft: req.body.paddingLeft,
|
||||||
|
paddingRight: req.body.paddingRight,
|
||||||
|
paddingTop: req.body.paddingTop,
|
||||||
|
paddingBottom: req.body.paddingBottom,
|
||||||
};
|
};
|
||||||
// Create audit log before updating
|
// Create audit log before updating
|
||||||
const result = await editObject({
|
const result = await editObject({
|
||||||
@ -143,6 +148,11 @@ export const newDocumentSizeRouteHandler = async (req, res) => {
|
|||||||
width: req.body.width,
|
width: req.body.width,
|
||||||
height: req.body.height,
|
height: req.body.height,
|
||||||
infiniteHeight: req.body.infiniteHeight,
|
infiniteHeight: req.body.infiniteHeight,
|
||||||
|
printPadding: req.body.printPadding,
|
||||||
|
paddingLeft: req.body.paddingLeft,
|
||||||
|
paddingRight: req.body.paddingRight,
|
||||||
|
paddingTop: req.body.paddingTop,
|
||||||
|
paddingBottom: req.body.paddingBottom,
|
||||||
};
|
};
|
||||||
const result = await newObject({
|
const result = await newObject({
|
||||||
model: documentSizeModel,
|
model: documentSizeModel,
|
||||||
|
|||||||
@ -20,6 +20,27 @@ import { formatTemplateContent } from '../../templates/templateformatter.js';
|
|||||||
const logger = log4js.getLogger('Document Templates');
|
const logger = log4js.getLogger('Document Templates');
|
||||||
logger.level = config.server.logLevel;
|
logger.level = config.server.logLevel;
|
||||||
|
|
||||||
|
function parseOptionalBoolean(value) {
|
||||||
|
if (value === undefined || value === null || value === '') {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value === true || value === false) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const lower = String(value).trim().toLowerCase();
|
||||||
|
if (lower === 'true' || lower === '1') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (lower === 'false' || lower === '0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaddingOption(req, defaultValue = true) {
|
||||||
|
return parseOptionalBoolean(req.body?.padding ?? req.query?.padding) ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
export const listDocumentTemplatesRouteHandler = async (
|
export const listDocumentTemplatesRouteHandler = async (
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@ -293,6 +314,7 @@ export const previewDocumentTemplateRouteHandler = async (req, res) => {
|
|||||||
const content = req.body?.content;
|
const content = req.body?.content;
|
||||||
const testObject = req.body?.testObject || req.body?.object || {};
|
const testObject = req.body?.testObject || req.body?.object || {};
|
||||||
const scale = req.body?.scale ?? 1;
|
const scale = req.body?.scale ?? 1;
|
||||||
|
const padding = getPaddingOption(req, true);
|
||||||
|
|
||||||
logger.debug(`Previewing document template with ID: ${id}`);
|
logger.debug(`Previewing document template with ID: ${id}`);
|
||||||
|
|
||||||
@ -301,7 +323,7 @@ export const previewDocumentTemplateRouteHandler = async (req, res) => {
|
|||||||
content,
|
content,
|
||||||
testObject,
|
testObject,
|
||||||
scale,
|
scale,
|
||||||
{},
|
{ padding },
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -320,7 +342,9 @@ export const downloadDocumentTemplateRouteHandler = async (req, res) => {
|
|||||||
const data = req.body?.object || req.body?.testObject || {};
|
const data = req.body?.object || req.body?.testObject || {};
|
||||||
const width = req.body?.width || req.query.width;
|
const width = req.body?.width || req.query.width;
|
||||||
const filename = req.body?.filename || req.query.filename || 'document';
|
const filename = req.body?.filename || req.query.filename || 'document';
|
||||||
const options = {};
|
const options = {
|
||||||
|
padding: getPaddingOption(req, true),
|
||||||
|
};
|
||||||
if (width) {
|
if (width) {
|
||||||
options.width = Number(width);
|
options.width = Number(width);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,4 +59,25 @@ describe('formatTemplateContent', () => {
|
|||||||
expect(result.content).not.toMatch(/\(\s+<%/);
|
expect(result.content).not.toMatch(/\(\s+<%/);
|
||||||
expect(result.content).not.toMatch(/%>\s+\)/);
|
expect(result.content).not.toMatch(/%>\s+\)/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not add a space next to a colon before EJS output', () => {
|
||||||
|
const input =
|
||||||
|
'<Barcode format="code128" height="33px" barcodeWidth="3">INV: <%= _reference %>\n </Barcode>';
|
||||||
|
const result = formatTemplateContent(input);
|
||||||
|
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'<Barcode format="code128" height="33px" barcodeWidth="3">INV:<%= _reference %></Barcode>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep barcode content and the closing tag on one line', () => {
|
||||||
|
const input =
|
||||||
|
'<Barcode format="code128" height="33px" barcodeWidth="3">INV:<%= _reference %></Barcode>';
|
||||||
|
const result = formatTemplateContent(input);
|
||||||
|
|
||||||
|
expect(result.content).toBe(
|
||||||
|
'<Barcode format="code128" height="33px" barcodeWidth="3">INV:<%= _reference %></Barcode>'
|
||||||
|
);
|
||||||
|
expect(result.content).not.toMatch(/%>\s*\n\s*<\/Barcode>/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -91,11 +91,51 @@ jest.unstable_mockModule('../../config.js', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { TemplateManager } = await import('../templatemanager.js');
|
const { TemplateManager, resolveDocumentPadding } = await import('../templatemanager.js');
|
||||||
const { getObject } = await import('../../database/database.js');
|
const { getObject } = await import('../../database/database.js');
|
||||||
const ejs = (await import('ejs')).default;
|
const ejs = (await import('ejs')).default;
|
||||||
const { generatePDF } = await import('../pdffactory.js');
|
const { generatePDF } = await import('../pdffactory.js');
|
||||||
|
|
||||||
|
describe('resolveDocumentPadding', () => {
|
||||||
|
const paddedSize = {
|
||||||
|
printPadding: false,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 8,
|
||||||
|
paddingTop: 6,
|
||||||
|
paddingBottom: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('does not apply padding unless requested or printPadding is true', () => {
|
||||||
|
expect(resolveDocumentPadding(paddedSize, false)).toEqual({
|
||||||
|
applyPadding: false,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies padding when requested', () => {
|
||||||
|
expect(resolveDocumentPadding(paddedSize, true)).toEqual({
|
||||||
|
applyPadding: true,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 8,
|
||||||
|
paddingTop: 6,
|
||||||
|
paddingBottom: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies padding when printPadding is true even if not requested', () => {
|
||||||
|
expect(resolveDocumentPadding({ ...paddedSize, printPadding: true }, false)).toEqual({
|
||||||
|
applyPadding: true,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 8,
|
||||||
|
paddingTop: 6,
|
||||||
|
paddingBottom: 4,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('TemplateManager', () => {
|
describe('TemplateManager', () => {
|
||||||
let templateManager;
|
let templateManager;
|
||||||
|
|
||||||
@ -161,6 +201,87 @@ describe('TemplateManager', () => {
|
|||||||
expect(generatePDF).toHaveBeenCalled();
|
expect(generatePDF).toHaveBeenCalled();
|
||||||
expect(result.pdf).toBeDefined();
|
expect(result.pdf).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds padding to page size when padding is requested', async () => {
|
||||||
|
getObject.mockResolvedValue({
|
||||||
|
documentSize: {
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
infiniteHeight: false,
|
||||||
|
printPadding: false,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 10,
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingBottom: 5,
|
||||||
|
},
|
||||||
|
global: false,
|
||||||
|
objectType: 'printer',
|
||||||
|
});
|
||||||
|
|
||||||
|
await templateManager.renderPDF('temp-id', 'content', {}, { padding: true });
|
||||||
|
|
||||||
|
expect(generatePDF).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
width: 120,
|
||||||
|
height: 60,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds padding when printPadding is true even if padding is false', async () => {
|
||||||
|
getObject.mockResolvedValue({
|
||||||
|
documentSize: {
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
infiniteHeight: false,
|
||||||
|
printPadding: true,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 10,
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingBottom: 5,
|
||||||
|
},
|
||||||
|
global: false,
|
||||||
|
objectType: 'printer',
|
||||||
|
});
|
||||||
|
|
||||||
|
await templateManager.renderPDF('temp-id', 'content', {}, { padding: false });
|
||||||
|
|
||||||
|
expect(generatePDF).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
width: 120,
|
||||||
|
height: 60,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the original page size when padding is not applied', async () => {
|
||||||
|
getObject.mockResolvedValue({
|
||||||
|
documentSize: {
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
infiniteHeight: false,
|
||||||
|
printPadding: false,
|
||||||
|
paddingLeft: 10,
|
||||||
|
paddingRight: 10,
|
||||||
|
paddingTop: 5,
|
||||||
|
paddingBottom: 5,
|
||||||
|
},
|
||||||
|
global: false,
|
||||||
|
objectType: 'printer',
|
||||||
|
});
|
||||||
|
|
||||||
|
await templateManager.renderPDF('temp-id', 'content', {}, { padding: false });
|
||||||
|
|
||||||
|
expect(generatePDF).toHaveBeenCalledWith(
|
||||||
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('renderDownload', () => {
|
describe('renderDownload', () => {
|
||||||
|
|||||||
@ -18,18 +18,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.previewWrapper {
|
.previewWrapper {
|
||||||
width: <%= (scaledWidth) || '50mm' %>;
|
width: <%= (scaledPageWidth) || (scaledWidth) || '50mm' %>;
|
||||||
height: <%= (scaledHeight) || '50mm' %>;
|
height: <%= (scaledPageHeight) || (scaledHeight) || '50mm' %>;
|
||||||
}
|
}
|
||||||
.previewDocument {
|
.previewDocument {
|
||||||
width: <%= (width) || '50mm' %>;
|
width: <%= (width) || '50mm' %>;
|
||||||
height: <%= (height) || '50mm' %>;
|
height: <%= (height) || '50mm' %>;
|
||||||
|
padding: <%= (paddingTop) || '0mm' %> <%= (paddingRight) || '0mm' %> <%= (paddingBottom) || '0mm' %> <%= (paddingLeft) || '0mm' %>;
|
||||||
|
box-sizing: content-box;
|
||||||
transform: scale(<%= scale || '1' %>);
|
transform: scale(<%= scale || '1' %>);
|
||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
}
|
}
|
||||||
.renderDocument {
|
.renderDocument {
|
||||||
width: <%= (scaledWidth) || '50mm' %>;
|
width: <%= (width) || '50mm' %>;
|
||||||
height: <%= (scaledHeight) || '50mm' %>;
|
height: <%= (height) || '50mm' %>;
|
||||||
|
padding: <%= (paddingTop) || '0mm' %> <%= (paddingRight) || '0mm' %> <%= (paddingBottom) || '0mm' %> <%= (paddingLeft) || '0mm' %>;
|
||||||
|
box-sizing: content-box;
|
||||||
transform: scale(<%= scale || '1' %>);
|
transform: scale(<%= scale || '1' %>);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -25,6 +25,11 @@ body {
|
|||||||
transform-origin: top left;
|
transform-origin: top left;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderDocument {
|
||||||
|
box-sizing: content-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.documentText {
|
.documentText {
|
||||||
|
|||||||
@ -4,7 +4,9 @@ const { html: beautifyHtml, js: beautifyJs } = beautify;
|
|||||||
|
|
||||||
const EJS_BLOCK_REGEX = /<%[\s\S]*?%>/g;
|
const EJS_BLOCK_REGEX = /<%[\s\S]*?%>/g;
|
||||||
const EJS_TAG_REGEX = /^<%([=#-]?)([\s\S]*?)%>/;
|
const EJS_TAG_REGEX = /^<%([=#-]?)([\s\S]*?)%>/;
|
||||||
const EJS_PLACEHOLDER_REGEX = /<!--EJS_PH_(\d+)-->/g;
|
const EJS_COMMENT_PLACEHOLDER_REGEX = /<!--EJS_PH_(\d+)-->/g;
|
||||||
|
const EJS_INLINE_PLACEHOLDER_REGEX = /___EJS_PH_(\d+)___/g;
|
||||||
|
const TEXT_ONLY_TAG_REGEX = /<([A-Za-z][\w:-]*)(\s[^>]*)?>([^<]*)<\/\1>/g;
|
||||||
|
|
||||||
const HTML_BEAUTIFY_OPTIONS = {
|
const HTML_BEAUTIFY_OPTIONS = {
|
||||||
indent_size: 2,
|
indent_size: 2,
|
||||||
@ -47,28 +49,53 @@ function formatEjsTag(tag) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isControlEjs(tag) {
|
||||||
|
const match = tag.match(EJS_TAG_REGEX);
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, modifier] = match;
|
||||||
|
return !modifier;
|
||||||
|
}
|
||||||
|
|
||||||
function maskEjsBlocks(content) {
|
function maskEjsBlocks(content) {
|
||||||
const ejsBlocks = [];
|
const ejsBlocks = [];
|
||||||
|
|
||||||
const masked = content.replace(EJS_BLOCK_REGEX, (match) => {
|
const masked = content.replace(EJS_BLOCK_REGEX, (match) => {
|
||||||
const index = ejsBlocks.length;
|
const index = ejsBlocks.length;
|
||||||
ejsBlocks.push(match);
|
ejsBlocks.push(match);
|
||||||
return `\n<!--EJS_PH_${index}-->\n`;
|
if (isControlEjs(match)) {
|
||||||
|
return `\n<!--EJS_PH_${index}-->\n`;
|
||||||
|
}
|
||||||
|
return `___EJS_PH_${index}___`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { masked, ejsBlocks };
|
return { masked, ejsBlocks };
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreEjsBlocks(content, ejsBlocks) {
|
function restoreEjsBlocks(content, ejsBlocks) {
|
||||||
return content.replace(EJS_PLACEHOLDER_REGEX, (_, index) =>
|
const restore = (_, index) => formatEjsTag(ejsBlocks[Number(index)]);
|
||||||
formatEjsTag(ejsBlocks[Number(index)])
|
return content
|
||||||
);
|
.replace(EJS_COMMENT_PLACEHOLDER_REGEX, restore)
|
||||||
|
.replace(EJS_INLINE_PLACEHOLDER_REGEX, restore);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collapseTextOnlyTags(content) {
|
||||||
|
return content.replace(TEXT_ONLY_TAG_REGEX, (_, tagName, attrs, inner) => {
|
||||||
|
const collapsedInner = inner.replace(/^\s+|\s+$/g, '');
|
||||||
|
return `<${tagName}${attrs || ''}>${collapsedInner}</${tagName}>`;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function tightenParenthesesAroundEjs(content) {
|
function tightenParenthesesAroundEjs(content) {
|
||||||
return content.replace(/\(\s+(<%)/g, '($1').replace(/(%>)\s+\)/g, '$1)');
|
return content.replace(/\(\s+(<%)/g, '($1').replace(/(%>)\s+\)/g, '$1)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function tightenColonsAroundEjs(content) {
|
||||||
|
return content.replace(/:\s+(<%)/g, ':$1');
|
||||||
|
}
|
||||||
|
|
||||||
export function formatTemplateContent(content) {
|
export function formatTemplateContent(content) {
|
||||||
if (content == null || typeof content !== 'string') {
|
if (content == null || typeof content !== 'string') {
|
||||||
return { error: 'Content is required and must be a string.', code: 400 };
|
return { error: 'Content is required and must be a string.', code: 400 };
|
||||||
@ -81,8 +108,10 @@ export function formatTemplateContent(content) {
|
|||||||
try {
|
try {
|
||||||
const { masked, ejsBlocks } = maskEjsBlocks(content);
|
const { masked, ejsBlocks } = maskEjsBlocks(content);
|
||||||
let formatted = beautifyHtml(masked, HTML_BEAUTIFY_OPTIONS);
|
let formatted = beautifyHtml(masked, HTML_BEAUTIFY_OPTIONS);
|
||||||
|
formatted = collapseTextOnlyTags(formatted);
|
||||||
formatted = restoreEjsBlocks(formatted, ejsBlocks);
|
formatted = restoreEjsBlocks(formatted, ejsBlocks);
|
||||||
formatted = tightenParenthesesAroundEjs(formatted);
|
formatted = tightenParenthesesAroundEjs(formatted);
|
||||||
|
formatted = tightenColonsAroundEjs(formatted);
|
||||||
return { content: formatted.trimEnd() };
|
return { content: formatted.trimEnd() };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { error: error.message || 'Failed to format template content.', code: 400 };
|
return { error: error.message || 'Failed to format template content.', code: 400 };
|
||||||
|
|||||||
@ -357,6 +357,32 @@ function imageToSvg(pngBuffer, widthPx, heightPx, documentWidthMm, documentHeigh
|
|||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toPaddingMm(value) {
|
||||||
|
const numeric = Number(value);
|
||||||
|
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveDocumentPadding(documentSize = {}, paddingRequested = false) {
|
||||||
|
const applyPadding = documentSize.printPadding == true || paddingRequested == true;
|
||||||
|
if (!applyPadding) {
|
||||||
|
return {
|
||||||
|
applyPadding: false,
|
||||||
|
paddingLeft: 0,
|
||||||
|
paddingRight: 0,
|
||||||
|
paddingTop: 0,
|
||||||
|
paddingBottom: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
applyPadding: true,
|
||||||
|
paddingLeft: toPaddingMm(documentSize.paddingLeft),
|
||||||
|
paddingRight: toPaddingMm(documentSize.paddingRight),
|
||||||
|
paddingTop: toPaddingMm(documentSize.paddingTop),
|
||||||
|
paddingBottom: toPaddingMm(documentSize.paddingBottom),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export class TemplateManager {
|
export class TemplateManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.fc = {
|
this.fc = {
|
||||||
@ -368,9 +394,10 @@ export class TemplateManager {
|
|||||||
|
|
||||||
async renderTemplate(id, content, data = {}, scale = 1, options = {}, preview = true) {
|
async renderTemplate(id, content, data = {}, scale = 1, options = {}, preview = true) {
|
||||||
try {
|
try {
|
||||||
|
const { padding: paddingRequested = false, ...ejsOptions } = options;
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
async: true,
|
async: true,
|
||||||
...options,
|
...ejsOptions,
|
||||||
};
|
};
|
||||||
logger.debug('Rendering template:', id);
|
logger.debug('Rendering template:', id);
|
||||||
|
|
||||||
@ -477,6 +504,11 @@ export class TemplateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const infiniteHeight = documentSize.infiniteHeight == true;
|
const infiniteHeight = documentSize.infiniteHeight == true;
|
||||||
|
const padding = resolveDocumentPadding(documentSize, paddingRequested == true);
|
||||||
|
const pageWidth = (documentSize.width || 0) + padding.paddingLeft + padding.paddingRight;
|
||||||
|
const pageHeight = infiniteHeight
|
||||||
|
? null
|
||||||
|
: (documentSize.height || 0) + padding.paddingTop + padding.paddingBottom;
|
||||||
|
|
||||||
const baseHtml = await ejs.render(
|
const baseHtml = await ejs.render(
|
||||||
baseTemplate,
|
baseTemplate,
|
||||||
@ -486,6 +518,12 @@ export class TemplateManager {
|
|||||||
height: infiniteHeight ? 'fit-content' : `${documentSize.height}mm`,
|
height: infiniteHeight ? 'fit-content' : `${documentSize.height}mm`,
|
||||||
scaledWidth: `${documentSize.width * scale}mm`,
|
scaledWidth: `${documentSize.width * scale}mm`,
|
||||||
scaledHeight: infiniteHeight ? 'auto' : `${documentSize.height * scale}mm`,
|
scaledHeight: infiniteHeight ? 'auto' : `${documentSize.height * scale}mm`,
|
||||||
|
scaledPageWidth: `${pageWidth * scale}mm`,
|
||||||
|
scaledPageHeight: infiniteHeight ? 'auto' : `${pageHeight * scale}mm`,
|
||||||
|
paddingTop: `${padding.paddingTop}mm`,
|
||||||
|
paddingRight: `${padding.paddingRight}mm`,
|
||||||
|
paddingBottom: `${padding.paddingBottom}mm`,
|
||||||
|
paddingLeft: `${padding.paddingLeft}mm`,
|
||||||
scale: `${scale}`,
|
scale: `${scale}`,
|
||||||
baseCSS: baseCSS,
|
baseCSS: baseCSS,
|
||||||
previewPaginationScript: preview ? previewPaginationScript : '',
|
previewPaginationScript: preview ? previewPaginationScript : '',
|
||||||
@ -495,9 +533,14 @@ export class TemplateManager {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
html: baseHtml,
|
html: baseHtml,
|
||||||
width: documentSize.width,
|
width: pageWidth,
|
||||||
height: infiniteHeight ? 'auto' : documentSize.height,
|
height: infiniteHeight ? 'auto' : pageHeight,
|
||||||
infiniteHeight: infiniteHeight,
|
infiniteHeight: infiniteHeight,
|
||||||
|
printPadding: documentSize.printPadding == true,
|
||||||
|
paddingLeft: padding.paddingLeft,
|
||||||
|
paddingRight: padding.paddingRight,
|
||||||
|
paddingTop: padding.paddingTop,
|
||||||
|
paddingBottom: padding.paddingBottom,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@ -548,12 +591,13 @@ export class TemplateManager {
|
|||||||
try {
|
try {
|
||||||
logger.debug(`Rendering ${format} for template:`, id);
|
logger.debug(`Rendering ${format} for template:`, id);
|
||||||
|
|
||||||
|
const { padding: _padding, ...imageOptions } = options;
|
||||||
const pdfResult = await this.renderPDF(id, content, data, options);
|
const pdfResult = await this.renderPDF(id, content, data, options);
|
||||||
if (pdfResult.error != undefined) {
|
if (pdfResult.error != undefined) {
|
||||||
return { error: pdfResult.error, code: pdfResult.code };
|
return { error: pdfResult.error, code: pdfResult.code };
|
||||||
}
|
}
|
||||||
|
|
||||||
const images = await convertPDFToImage(pdfResult.pdf, options);
|
const images = await convertPDFToImage(pdfResult.pdf, imageOptions);
|
||||||
const converted = [];
|
const converted = [];
|
||||||
for (const image of images) {
|
for (const image of images) {
|
||||||
if (format === 'png') {
|
if (format === 'png') {
|
||||||
@ -573,12 +617,13 @@ export class TemplateManager {
|
|||||||
try {
|
try {
|
||||||
logger.debug('Rendering SVG for template:', id);
|
logger.debug('Rendering SVG for template:', id);
|
||||||
|
|
||||||
|
const { padding: _padding, ...imageOptions } = options;
|
||||||
const pdfResult = await this.renderPDF(id, content, data, options);
|
const pdfResult = await this.renderPDF(id, content, data, options);
|
||||||
if (pdfResult.error != undefined) {
|
if (pdfResult.error != undefined) {
|
||||||
return { error: pdfResult.error, code: pdfResult.code };
|
return { error: pdfResult.error, code: pdfResult.code };
|
||||||
}
|
}
|
||||||
|
|
||||||
const images = await convertPDFToImage(pdfResult.pdf, options);
|
const images = await convertPDFToImage(pdfResult.pdf, imageOptions);
|
||||||
const svgs = [];
|
const svgs = [];
|
||||||
for (const image of images) {
|
for (const image of images) {
|
||||||
const png = await sharp(image).png().toBuffer();
|
const png = await sharp(image).png().toBuffer();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user