From 7e16bc89193155244612156a4647842509ba361f Mon Sep 17 00:00:00 2001 From: Rodrigo Martins Date: Mon, 21 Apr 2025 20:24:54 -0300 Subject: [PATCH] tenta fix pdfservice.js --- src/server/services/PdfService.ts | 156 ++++++++++++++---------------- 1 file changed, 74 insertions(+), 82 deletions(-) diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index 6c9d2fe..3970d1f 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -287,96 +287,72 @@ export class PdfService { if (!fontItalic) fontItalic = await pdfDoc.embedFont(StandardFonts.HelveticaOblique); if (!fontBoldItalic) fontBoldItalic = fontBold; - // --- NOVO PARSER HTML TinyMCE PARA BLOCOS --- - // Usa jsdom para manter estrutura de blocos e estilos - const { JSDOM } = require('jsdom'); - const dom = new JSDOM(`${conteudo}`); - const body = dom.window.document.body; - - // Função auxiliar para obter estilos relevantes - function getBlockStyles(el: HTMLElement) { - const styles: any = {}; - const computed = el.getAttribute('style') || ''; - if (/font-weight:\s*bold/i.test(computed) || el.tagName === 'B' || el.tagName === 'STRONG') styles.bold = true; - if (/font-style:\s*italic/i.test(computed) || el.tagName === 'I' || el.tagName === 'EM') styles.italic = true; - if (/text-decoration:\s*underline/i.test(computed) || el.tagName === 'U') styles.underline = true; - if (/text-align:\s*center/i.test(computed) || el.align === 'center' || el.tagName === 'CENTER') styles.align = 'center'; - if (/text-align:\s*right/i.test(computed) || el.align === 'right') styles.align = 'right'; - if (/text-indent:\s*2em/i.test(computed)) styles.indent = true; - if (/font-family:\s*['"]?Fira Sans/i.test(computed)) styles.fontFamily = 'Fira Sans'; - if (el.tagName.match(/^H[1-6]$/)) styles.heading = el.tagName.toLowerCase(); - return styles; - } - - // Função para quebrar texto em linhas respeitando largura máxima - function breakLines(text: string, fontObj: any, fontSize: number, maxWidth: number) { - const words = text.split(' '); - const lines: string[] = []; - let currentLine = ''; - for (const word of words) { - const testLine = currentLine ? currentLine + ' ' + word : word; - const width = fontObj.widthOfTextAtSize(testLine, fontSize); - if (width > maxWidth && currentLine) { - lines.push(currentLine); - currentLine = word; - } else { - currentLine = testLine; - } - } - if (currentLine) lines.push(currentLine); - return lines; - } - - // Função para processar blocos do HTML - function parseBlocks(node: HTMLElement, parentStyles: any = {}) { + // --- PARSER HTML TinyMCE SEM DEPENDÊNCIAS EXTERNAS --- + // Suporte básico a blocos, alinhamento, recuo, negrito, itálico, sublinhado, listas, títulos + function simpleHtmlToBlocks(html: string): any[] { + // Remove tags não suportadas e preserva apenas blocos essenciais + html = html.replace(/]*class=["'][^"']*MsoNormal[^"']*["'][^>]*>/gi, '

'); + html = html.replace(/]*>/gi, '

'); + html = html.replace(/<\/div>/gi, '

'); + html = html.replace(//gi, '\n'); + html = html.replace(/<\/li>/gi, '\n'); + html = html.replace(/<\/p>/gi, '

\n'); + html = html.replace(/<\/h[1-6]>/gi, m => m + '\n'); + html = html.replace(/<\/ul>/gi, '\n'); + html = html.replace(/<\/ol>/gi, '\n'); + // Divide por linhas + const lines = html.split('\n'); const blocks: any[] = []; - for (const el of Array.from(node.childNodes)) { - if (el.nodeType === 3) { // texto puro - let text = el.textContent || ''; - if (text.trim() !== '') { - blocks.push({ text, tag: parentStyles.tag || 'span', styles: { ...parentStyles } }); - } - continue; - } - if (el.nodeType !== 1) continue; - const elem = el as HTMLElement; - let styles = { ...parentStyles, ...getBlockStyles(elem) }; - let tag = elem.tagName.toLowerCase(); - - if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'center'].includes(tag)) { - let blockText = ''; - elem.childNodes.forEach(child => { - if (child.nodeType === 3) blockText += child.textContent; - }); - blocks.push({ text: blockText, tag, styles: { ...styles } }); - blocks.push(...parseBlocks(elem, styles)); - blocks.push({ tag: 'br', text: '', styles: {} }); - continue; - } - if (tag === 'br') { - blocks.push({ tag: 'br', text: '', styles: { ...styles } }); - continue; - } - if (['b', 'strong', 'i', 'em', 'u', 'span'].includes(tag)) { - blocks.push(...parseBlocks(elem, styles)); - continue; - } + let inList = false, listType = ''; + for (let raw of lines) { + let line = raw.trim(); + if (!line) continue; // Listas - if (tag === 'ul' || tag === 'ol') { - elem.childNodes.forEach(li => { - if (li.nodeType === 1 && (li as HTMLElement).tagName === 'LI') { - blocks.push({ text: '• ' + (li.textContent || ''), tag: 'li', styles: { ...styles } }); - blocks.push({ tag: 'br', text: '', styles: {} }); - } - }); + if (line.match(/^]*>/i)) { inList = true; listType = 'ul'; continue; } + if (line.match(/^]*>/i)) { inList = true; listType = 'ol'; continue; } + if (line.match(/^<\/ul>/i) || line.match(/^<\/ol>/i)) { inList = false; listType = ''; continue; } + if (line.match(/^]*>/i)) { + let text = line.replace(/^]*>/i, '').replace(/<\/li>$/i, ''); + blocks.push({ text: (listType === 'ol' ? '1. ' : '• ') + text, tag: 'li', styles: {} }); continue; } + // Títulos + let headingMatch = line.match(/^]*>(.*?)<\/h\1>/i); + if (headingMatch) { + blocks.push({ text: headingMatch[2], tag: `h${headingMatch[1]}`, styles: { heading: `h${headingMatch[1]}` } }); + continue; + } + // Parágrafo + let pMatch = line.match(/^]*)>(.*?)<\/p>$/i); + if (pMatch) { + let styles: any = {}; + let styleAttr = pMatch[1]; + if (/text-align:\s*center/i.test(styleAttr)) styles.align = 'center'; + if (/text-align:\s*right/i.test(styleAttr)) styles.align = 'right'; + if (/text-indent:\s*2em/i.test(styleAttr)) styles.indent = true; + let text = pMatch[2]; + // Inline styles + if (/|/i.test(text)) styles.bold = true; + if (/|/i.test(text)) styles.italic = true; + if (//i.test(text)) styles.underline = true; + text = text.replace(/<\/?(b|strong|i|em|u)>/gi, ''); + blocks.push({ text, tag: 'p', styles }); + continue; + } + // Centralizado + let centerMatch = line.match(/^]*>(.*?)<\/center>$/i); + if (centerMatch) { + blocks.push({ text: centerMatch[1], tag: 'p', styles: { align: 'center' } }); + continue; + } + // Texto puro + blocks.push({ text: line.replace(/<\/?[^>]+>/g, ''), tag: 'span', styles: {} }); } return blocks; } // Gera os blocos do HTML do TinyMCE - const blocks = parseBlocks(body); + const blocks = simpleHtmlToBlocks(conteudo); // Renderização dos blocos no PDF let pageIndex = 0; @@ -411,7 +387,23 @@ export class PdfService { const cleanText = (block.text || '').replace(/\r?\n/g, ' '); // Quebra em múltiplas linhas se necessário - const lines = breakLines(cleanText, usedFont, blockFontSize, MAX_WIDTH); + const lines = (() => { + const words = cleanText.split(' '); + const result: string[] = []; + let currentLine = ''; + for (const word of words) { + const testLine = currentLine ? currentLine + ' ' + word : word; + const width = usedFont.widthOfTextAtSize(testLine, blockFontSize); + if (width > MAX_WIDTH && currentLine) { + result.push(currentLine); + currentLine = word; + } else { + currentLine = testLine; + } + } + if (currentLine) result.push(currentLine); + return result; + })(); for (const line of lines) { let x = margens.left + (block.styles.indent ? FONT_SIZE * 2 : 0);