ERPTurbo_Poster/lib/utils.js
shenyifei dc940d2598 feat(api): 添加海报和PDF生成功能
- 新增海报生成接口,支持从网页URL或HTML内容生成海报图像
- 新增PDF生成接口,支持从网页URL或HTML内容生成PDF文档
- 添加Swagger API文档注释,完善接口描述和参数说明
- 实现HTML内容参数支持,允许直接传入HTML结构生成海报/PDF
- 添加输入验证和标准化响应格式
- 引入DOMPurify库对HTML内容进行安全过滤
- 更新环境变量配置,支持API密钥认证和CORS设置
- 优化上传逻辑,统一返回标准响应结构
- 添加构建脚本支持Docker镜像打包和推送
2025-11-20 17:51:35 +08:00

167 lines
5.5 KiB
JavaScript

/**
* 将JSON格式的HTML结构转换为HTML字符串
* @param {Object} json - JSON格式的HTML结构
* @returns {string} HTML字符串
*/
function jsonToHtml(json) {
if (!json || typeof json !== 'object') {
return '';
}
function parseElement(obj) {
if (obj === null || obj === undefined) {
return '';
}
if (typeof obj === 'string') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => parseElement(item)).join('');
}
// 检查是否为文本节点 (#text)
if ('#text' in obj) {
let tagName = 'div'; // 默认标签
let attributes = '';
let content = obj['#text'] || '';
// 处理属性和其他设置
for (const key in obj) {
if (key === '#text') continue;
if (key.startsWith('@')) {
// 属性 (@class, @id等)
const attrName = key.substring(1);
attributes += ` ${attrName}="${obj[key]}"`;
} else if (['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'header', 'footer', 'main', 'article', 'aside'].includes(key)) {
// 子元素标签
tagName = key;
content = parseElement(obj[key]);
}
}
return `<${tagName}${attributes}>${content}</${tagName}>`;
}
// 普通元素节点
let tagName = 'div'; // 默认标签
let attributes = '';
let content = '';
for (const key in obj) {
if (key.startsWith('@')) {
// 属性 (@class, @id等)
const attrName = key.substring(1);
attributes += ` ${attrName}="${obj[key]}"`;
} else if (['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'section', 'header', 'footer', 'main', 'article', 'aside', 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'td', 'th'].includes(key)) {
// 明确指定的标签类型
tagName = key;
content = parseElement(obj[key]);
} else if (key === 'style') {
// style标签特殊处理
content += `<style>${obj[key]}</style>`;
} else if (key === 'title') {
// title标签特殊处理
content += `<title>${obj[key]}</title>`;
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
// 其他嵌套对象
content += parseElement(obj[key]);
} else if (key !== '#text') {
// 其他属性
attributes += ` ${key}="${obj[key]}"`;
}
}
return `<${tagName}${attributes}>${content}</${tagName}>`;
}
// 处理顶层html结构
if (json.html) {
const headContent = json.html.head ? parseElement(json.html.head) : '';
const bodyContent = json.html.body ? parseElement(json.html.body) : '';
const headSection = headContent ? `<head>${headContent}</head>` : '<head></head>';
const bodySection = bodyContent ? `<body>${bodyContent}</body>` : '<body></body>';
return `<!DOCTYPE html><html>${headSection}${bodySection}</html>`;
}
if (json.head || json.body) {
const headContent = json.head ? parseElement(json.head) : '';
const bodyContent = json.body ? parseElement(json.body) : '';
const headSection = headContent ? `<head>${headContent}</head>` : '<head></head>';
const bodySection = bodyContent ? `<body>${bodyContent}</body>` : '<body></body>';
return `<!DOCTYPE html><html>${headSection}${bodySection}</html>`;
}
return parseElement(json);
}
/**
* 生成随机字符串
* @param {number} length - 字符串长度
* @returns {string} 随机字符串
*/
function randomString(length) {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
/**
* Sanitizes HTML content by removing potentially dangerous elements and attributes
* @param {string} html - HTML content to sanitize
* @returns {string} Sanitized HTML content
*/
async function sanitizeHtml(html) {
if (typeof html !== 'string') {
return '';
}
// Dynamically import DOMPurify and JSDOM
const { JSDOM } = await import('jsdom');
const { default: DOMPurify } = await import('dompurify');
// Create a window object from jsdom for DOMPurify
const window = new JSDOM('').window;
const purify = DOMPurify(window);
// Define allowed tags and attributes for posters
const allowedTags = [
'div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'section', 'header', 'footer', 'main', 'article', 'aside',
'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'td', 'th',
'strong', 'em', 'b', 'i', 'u', 's', 'small', 'sub', 'sup',
'a', 'img', 'br', 'hr', 'pre', 'code', 'blockquote', 'cite',
'figure', 'figcaption', 'mark', 'time', 'address', 'dl', 'dt', 'dd'
];
const allowedAttributes = [
'class', 'id', 'style', 'title', 'alt', 'href', 'src', 'width', 'height',
'colspan', 'rowspan', 'align', 'valign', 'scope', 'headers', 'abbr',
'datetime', 'cite', 'rel', 'target', 'name', 'value', 'type'
];
// Sanitize the HTML
const sanitized = purify.sanitize(html, {
ALLOWED_TAGS: allowedTags,
ALLOWED_ATTR: allowedAttributes,
FORBID_TAGS: ['script', 'object', 'embed', 'form', 'input', 'button', 'textarea', 'select', 'option', 'iframe', 'frame', 'frameset', 'applet', 'base', 'meta', 'link', 'noscript'],
FORBID_ATTR: ['on*', 'src*', 'href*', 'action*', 'data*', 'vbscript*', 'javascript*', 'expression', 'behavior', 'xmlns']
});
return sanitized;
}
export {
jsonToHtml,
randomString,
sanitizeHtml
};