- 新增海报生成接口,支持从网页URL或HTML内容生成海报图像 - 新增PDF生成接口,支持从网页URL或HTML内容生成PDF文档 - 添加Swagger API文档注释,完善接口描述和参数说明 - 实现HTML内容参数支持,允许直接传入HTML结构生成海报/PDF - 添加输入验证和标准化响应格式 - 引入DOMPurify库对HTML内容进行安全过滤 - 更新环境变量配置,支持API密钥认证和CORS设置 - 优化上传逻辑,统一返回标准响应结构 - 添加构建脚本支持Docker镜像打包和推送
167 lines
5.5 KiB
JavaScript
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
|
|
};
|