diff --git a/logic/normalize.js b/logic/normalize.js new file mode 100644 index 0000000..b72ecb9 --- /dev/null +++ b/logic/normalize.js @@ -0,0 +1,58 @@ +const NAMED_ENTITIES = { + ' ': ' ', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + ''': "'", + ''': "'", +}; + +const MAX_DECODE_ITERATIONS = 5; + +function decodeEntitiesOnce(text) { + let result = text.replace(/(\d+);/g, (match, code) => { + const num = Number(code); + return Number.isFinite(num) ? String.fromCodePoint(num) : match; + }); + result = result.replace(/([0-9a-fA-F]+);/g, (match, hex) => { + const num = parseInt(hex, 16); + return Number.isFinite(num) ? String.fromCodePoint(num) : match; + }); + for (const [entity, char] of Object.entries(NAMED_ENTITIES)) { + result = result.split(entity).join(char); + } + return result; +} + +function stripHtmlTags(text) { + return text.replace(/<[^>]*>/g, ' '); +} + +// Decodifica HTML/entities (inclusive escapado multiplas vezes, ate MAX_DECODE_ITERATIONS +// para nunca travar em loop) e remove tags -- sem apagar informacao (datas, nomes, numeros, +// codigos de erro, hostname, versao continuam intactos: nenhuma dessas regras mexe em texto +// que nao seja literalmente uma entidade HTML ou uma tag). +function normalizeGlpiText(rawText) { + if (rawText === null || rawText === undefined) return ''; + let text = String(rawText); + + for (let i = 0; i < MAX_DECODE_ITERATIONS; i += 1) { + const decoded = decodeEntitiesOnce(text); + if (decoded === text) break; + text = decoded; + } + + text = stripHtmlTags(text); + + text = text + .split('\n') + .map((line) => line.replace(/[ \t]+/g, ' ').trim()) + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); + + return text; +} + +module.exports = { normalizeGlpiText, decodeEntitiesOnce, stripHtmlTags }; diff --git a/logic/normalize.test.js b/logic/normalize.test.js new file mode 100644 index 0000000..01b4a71 --- /dev/null +++ b/logic/normalize.test.js @@ -0,0 +1,63 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { normalizeGlpiText } = require('./normalize'); + +test('HTML normal e removido, texto preservado', () => { + assert.equal(normalizeGlpiText('
Aguardando cliente
'), 'Aguardando cliente'); +}); + +test('entidades HTML escapadas uma vez sao decodificadas e as tags removidas', () => { + const entrada = '<p>Aguardando retorno de Patricia sobre troca da carcaça</p>'; + assert.equal(normalizeGlpiText(entrada), 'Aguardando retorno de Patricia sobre troca da carcaça'); +}); + +test('entidades HTML escapadas duas vezes sao decodificadas iterativamente', () => { + const entrada = '<p>texto</p>'; + assert.equal(normalizeGlpiText(entrada), 'texto'); +}); + +test('entidades nomeadas comuns (amp, nbsp) sao decodificadas', () => { + assert.equal(normalizeGlpiText('Suporte & Manutenção'), 'Suporte & Manutenção'); +}); + +test('texto sem HTML permanece intacto (so trim de espacos)', () => { + assert.equal(normalizeGlpiText(' Servidor não responde '), 'Servidor não responde'); +}); + +test('caracteres acentuados sao preservados', () => { + assert.equal(normalizeGlpiText('Impressão não está funcionando corretamente na área de atendimento
'), 'Impressão não está funcionando corretamente na área de atendimento'); +}); + +test('codigo tecnico (numeros, versao, hostname) e preservado', () => { + const entrada = 'Erro 0x8007000E no servidor SRV-FILE01, versao 10.2.3, IP 192.168.1.50
'; + assert.equal(normalizeGlpiText(entrada), 'Erro 0x8007000E no servidor SRV-FILE01, versao 10.2.3, IP 192.168.1.50'); +}); + +test('string vazia retorna string vazia', () => { + assert.equal(normalizeGlpiText(''), ''); +}); + +test('null e undefined retornam string vazia sem lancar excecao', () => { + assert.doesNotThrow(() => normalizeGlpiText(null)); + assert.doesNotThrow(() => normalizeGlpiText(undefined)); + assert.equal(normalizeGlpiText(null), ''); + assert.equal(normalizeGlpiText(undefined), ''); +}); + +test('quebras de linha excessivas e espacos duplicados sao colapsados', () => { + const entrada = 'Linha 1
\n\n\n\nLinha 2 com espacos
'; + const resultado = normalizeGlpiText(entrada); + assert.ok(!resultado.includes('\n\n\n')); + assert.ok(!/ {2,}/.test(resultado)); +}); + +test('decodificacao para no limite de iteracoes sem lancar excecao (protecao contra loop)', () => { + const entradaMaliciosa = '&'.repeat(20) + '#60;p>'; + assert.doesNotThrow(() => normalizeGlpiText(entradaMaliciosa)); +}); + +test('conteudo normalizado de duas codificacoes diferentes do mesmo texto e identico', () => { + const versaoHtml = 'Aguardando cliente
'; + const versaoEscapada = '<p>Aguardando cliente</p>'; + assert.equal(normalizeGlpiText(versaoHtml), normalizeGlpiText(versaoEscapada)); +});