Skip to content

Commit 20669cb

Browse files
anabyefabiana-monteiro
authored andcommitted
feat(page-default): adiciona propriedade p-helper no subtítulo
Adiciona a propriedade 'p-helper' para exibição de mensagens de ajuda. Permite a formatação de texto no conteúdo do helper utilizando as tags HTML básicas: <b>, <strong>, <i>, <em> e <u>. Fixes DTHFUI-13013
1 parent f144b03 commit 20669cb

26 files changed

Lines changed: 953 additions & 116 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
export * from './po-helper.module';
22
export * from './po-helper.component';
33
export * from './interfaces/po-helper.interface';
4+
export * from './po-helper-content-utils';
5+
export * from './po-helper-content.pipe';

projects/ui/src/lib/components/po-helper/interfaces/po-helper.interface.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
/**
2-
* @usedBy PoHelperComponent
2+
* @usedBy PoHelperComponent, PoPageDefaultComponent
33
*
44
* @description
55
*
6-
* *Interface* que define as opções de configuração do componente po-helper.
7-
*
8-
* Permite customizar o conteúdo, título, tipo do ícone, modo de abertura do popover, ações customizadas e eventos.
9-
*
6+
* Interface para configuração das opções de ajuda (*helper*).
107
*/
118
export interface PoHelperOptions {
129
/**
@@ -26,6 +23,14 @@ export interface PoHelperOptions {
2623
* @description
2724
*
2825
* Texto explicativo exibido no popover.
26+
*
27+
* Suporta formatação básica com as tags `<b>` (negrito), `<strong>` (negrito), `<i>` (itálico), `<em>` (itálico) e
28+
* `<u>` (sublinhado).
29+
*
30+
* Exemplo:
31+
* ```typescript
32+
* content: 'Texto <b>importante</b> com <em>destaque</em> e <u>sublinhado</u>'
33+
* ```
2934
*/
3035
content?: string;
3136

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { parseHelperContent } from './po-helper-content-utils';
2+
3+
describe('PoHelperSafeParser', () => {
4+
describe('parseHelperContent', () => {
5+
it('should return empty array for null/undefined/empty content', () => {
6+
expect(parseHelperContent(null)).toEqual([]);
7+
expect(parseHelperContent(undefined)).toEqual([]);
8+
expect(parseHelperContent('')).toEqual([]);
9+
});
10+
11+
it('should return single fragment for plain text without tags', () => {
12+
const result = parseHelperContent('Texto simples');
13+
expect(result).toEqual([{ text: 'Texto simples', bold: false, italic: false, underline: false }]);
14+
});
15+
16+
it('should parse bold tag correctly', () => {
17+
const result = parseHelperContent('Texto <b>negrito</b> normal');
18+
expect(result).toEqual([
19+
{ text: 'Texto ', bold: false, italic: false, underline: false },
20+
{ text: 'negrito', bold: true, italic: false, underline: false },
21+
{ text: ' normal', bold: false, italic: false, underline: false }
22+
]);
23+
});
24+
25+
it('should parse italic tag correctly', () => {
26+
const result = parseHelperContent('Texto <i>itálico</i> normal');
27+
expect(result).toEqual([
28+
{ text: 'Texto ', bold: false, italic: false, underline: false },
29+
{ text: 'itálico', bold: false, italic: true, underline: false },
30+
{ text: ' normal', bold: false, italic: false, underline: false }
31+
]);
32+
});
33+
34+
it('should parse underline tag correctly', () => {
35+
const result = parseHelperContent('Texto <u>sublinhado</u> normal');
36+
expect(result).toEqual([
37+
{ text: 'Texto ', bold: false, italic: false, underline: false },
38+
{ text: 'sublinhado', bold: false, italic: false, underline: true },
39+
{ text: ' normal', bold: false, italic: false, underline: false }
40+
]);
41+
});
42+
43+
it('should parse nested tags correctly', () => {
44+
const result = parseHelperContent('<b><i>negrito e itálico</i></b>');
45+
expect(result).toEqual([{ text: 'negrito e itálico', bold: true, italic: true, underline: false }]);
46+
});
47+
48+
it('should parse multiple nested tags', () => {
49+
const result = parseHelperContent('<b><i><u>todos</u></i></b>');
50+
expect(result).toEqual([{ text: 'todos', bold: true, italic: true, underline: true }]);
51+
});
52+
53+
it('should handle mixed formatting in sequence', () => {
54+
const result = parseHelperContent('<b>negrito</b> e <i>itálico</i>');
55+
expect(result).toEqual([
56+
{ text: 'negrito', bold: true, italic: false, underline: false },
57+
{ text: ' e ', bold: false, italic: false, underline: false },
58+
{ text: 'itálico', bold: false, italic: true, underline: false }
59+
]);
60+
});
61+
62+
it('should sanitize disallowed tags (script)', () => {
63+
const result = parseHelperContent('Texto <script>alert("xss")</script> seguro');
64+
expect(result).toEqual([{ text: 'Texto alert("xss") seguro', bold: false, italic: false, underline: false }]);
65+
});
66+
67+
it('should sanitize disallowed tags (div, span, a)', () => {
68+
const result = parseHelperContent('<div>conteúdo</div> <span>texto</span> <a href="x">link</a>');
69+
expect(result).toEqual([{ text: 'conteúdo texto link', bold: false, italic: false, underline: false }]);
70+
});
71+
72+
it('should sanitize tags with attributes', () => {
73+
const result = parseHelperContent('<b style="color:red">negrito</b>');
74+
expect(result).toEqual([{ text: 'negrito', bold: true, italic: false, underline: false }]);
75+
});
76+
77+
it('should handle unclosed tags gracefully', () => {
78+
const result = parseHelperContent('<b>negrito sem fechar');
79+
expect(result).toEqual([{ text: 'negrito sem fechar', bold: true, italic: false, underline: false }]);
80+
});
81+
82+
it('should handle extra closing tags gracefully', () => {
83+
const result = parseHelperContent('texto</b> normal');
84+
expect(result).toEqual([
85+
{ text: 'texto', bold: false, italic: false, underline: false },
86+
{ text: ' normal', bold: false, italic: false, underline: false }
87+
]);
88+
});
89+
90+
it('should handle case-insensitive tags', () => {
91+
const result = parseHelperContent('<B>negrito</B> <I>itálico</I>');
92+
expect(result).toEqual([
93+
{ text: 'negrito', bold: true, italic: false, underline: false },
94+
{ text: ' ', bold: false, italic: false, underline: false },
95+
{ text: 'itálico', bold: false, italic: true, underline: false }
96+
]);
97+
});
98+
99+
it('should prevent XSS via event handlers in allowed tags', () => {
100+
const result = parseHelperContent('<b onmouseover="alert(1)">texto</b>');
101+
expect(result).toEqual([{ text: 'texto', bold: true, italic: false, underline: false }]);
102+
});
103+
104+
it('should prevent XSS via img tag with onerror', () => {
105+
const result = parseHelperContent('<img src=x onerror="alert(1)">texto');
106+
expect(result).toEqual([{ text: 'texto', bold: false, italic: false, underline: false }]);
107+
});
108+
109+
it('should handle partially overlapping tags', () => {
110+
const result = parseHelperContent('<b>bold <i>bold+italic</b> italic</i>');
111+
expect(result).toEqual([
112+
{ text: 'bold ', bold: true, italic: false, underline: false },
113+
{ text: 'bold+italic', bold: true, italic: true, underline: false },
114+
{ text: ' italic', bold: false, italic: true, underline: false }
115+
]);
116+
});
117+
118+
it('should parse <strong> as bold', () => {
119+
const result = parseHelperContent('Texto <strong>importante</strong> normal');
120+
expect(result).toEqual([
121+
{ text: 'Texto ', bold: false, italic: false, underline: false },
122+
{ text: 'importante', bold: true, italic: false, underline: false },
123+
{ text: ' normal', bold: false, italic: false, underline: false }
124+
]);
125+
});
126+
127+
it('should parse <em> as italic', () => {
128+
const result = parseHelperContent('Texto <em>enfatizado</em> normal');
129+
expect(result).toEqual([
130+
{ text: 'Texto ', bold: false, italic: false, underline: false },
131+
{ text: 'enfatizado', bold: false, italic: true, underline: false },
132+
{ text: ' normal', bold: false, italic: false, underline: false }
133+
]);
134+
});
135+
136+
it('should handle mixed <strong>, <em>, <b>, <i>, <u> together', () => {
137+
const result = parseHelperContent('<strong>bold</strong> <em>italic</em> <u>underline</u>');
138+
expect(result).toEqual([
139+
{ text: 'bold', bold: true, italic: false, underline: false },
140+
{ text: ' ', bold: false, italic: false, underline: false },
141+
{ text: 'italic', bold: false, italic: true, underline: false },
142+
{ text: ' ', bold: false, italic: false, underline: false },
143+
{ text: 'underline', bold: false, italic: false, underline: true }
144+
]);
145+
});
146+
});
147+
});
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* @description
3+
*
4+
* Utilitário de "Safe Parser" para o componente po-helper.
5+
*
6+
* Permite o uso de tags de formatação básica (`<b>`, `<strong>`, `<i>`, `<em>`, `<u>`) no conteúdo do helper, sem
7+
* utilizar `innerHTML`, garantindo proteção contra ataques XSS.
8+
*
9+
* Qualquer tag HTML que não esteja na whitelist é sanitizada (removida), preservando apenas o texto interno.
10+
*/
11+
12+
/**
13+
* Representa um fragmento de texto com suas propriedades de formatação.
14+
*/
15+
export interface PoHelperTextFragment {
16+
/** Conteúdo textual do fragmento */
17+
text: string;
18+
/** Indica se o fragmento deve ser exibido em negrito */
19+
bold: boolean;
20+
/** Indica se o fragmento deve ser exibido em itálico */
21+
italic: boolean;
22+
/** Indica se o fragmento deve ser exibido com sublinhado */
23+
underline: boolean;
24+
}
25+
26+
/** Tags permitidas pelo safe parser */
27+
const ALLOWED_TAGS = ['b', 'i', 'u', 'strong', 'em'];
28+
29+
/** Mapa de normalização: tags semânticas são convertidas para suas equivalentes de formatação */
30+
const TAG_NORMALIZE_MAP: Record<string, string> = {
31+
strong: 'b',
32+
em: 'i'
33+
};
34+
35+
/**
36+
* Regex para capturar tags HTML (abertura e fechamento).
37+
* Captura: tag name e se é fechamento (/).
38+
*/
39+
const TAG_REGEX = /<(\/?)(\w+)(?:\s[^>]*)?\/?>/gi;
40+
41+
/**
42+
* Remove todas as tags HTML que não estão na whitelist, preservando o texto interno.
43+
*
44+
* @param input String com possíveis tags HTML
45+
* @returns String com apenas as tags permitidas
46+
*/
47+
function sanitizeContent(input: string): string {
48+
return input.replace(TAG_REGEX, (_match, closing, tagName) => {
49+
const normalizedTag = tagName.toLowerCase();
50+
if (ALLOWED_TAGS.includes(normalizedTag)) {
51+
const outputTag = TAG_NORMALIZE_MAP[normalizedTag] || normalizedTag;
52+
return closing ? `</${outputTag}>` : `<${outputTag}>`;
53+
}
54+
return '';
55+
});
56+
}
57+
58+
/** Mapa de contadores por tag */
59+
interface TagCounters {
60+
b: number;
61+
i: number;
62+
u: number;
63+
}
64+
65+
/**
66+
* Incrementa o contador da tag de abertura.
67+
*/
68+
function incrementTagCounter(counters: TagCounters, tag: string): void {
69+
counters[tag]++;
70+
}
71+
72+
/**
73+
* Decrementa o contador da tag de fechamento.
74+
*/
75+
function decrementTagCounter(counters: TagCounters, tag: string): void {
76+
counters[tag] = Math.max(0, counters[tag] - 1);
77+
}
78+
79+
/**
80+
* Faz o parsing de uma string com tags de formatação permitidas (`<b>`, `<i>`, `<u>`)
81+
* e retorna um array de fragmentos com as propriedades de formatação aplicadas.
82+
*
83+
* Tags não permitidas são removidas (sanitizadas). Tags aninhadas são suportadas.
84+
*
85+
* @param content String de conteúdo com possíveis tags de formatação
86+
* @returns Array de fragmentos de texto com informações de formatação
87+
*/
88+
export function parseHelperContent(content: string): Array<PoHelperTextFragment> {
89+
if (!content) {
90+
return [];
91+
}
92+
93+
const sanitized = sanitizeContent(content);
94+
const fragments: Array<PoHelperTextFragment> = [];
95+
const counters: TagCounters = { b: 0, i: 0, u: 0 };
96+
97+
const splitRegex = /(<\/?[biu]>)/gi;
98+
const parts = sanitized.split(splitRegex);
99+
100+
for (const part of parts) {
101+
if (!part) {
102+
continue;
103+
}
104+
105+
const tagMatch = /^<(\/?)([biu])>$/i.exec(part);
106+
107+
if (tagMatch) {
108+
const isClosing = tagMatch[1] === '/';
109+
const tag = tagMatch[2].toLowerCase();
110+
if (isClosing) {
111+
decrementTagCounter(counters, tag);
112+
} else {
113+
incrementTagCounter(counters, tag);
114+
}
115+
} else {
116+
fragments.push({
117+
text: part,
118+
bold: counters.b > 0,
119+
italic: counters.i > 0,
120+
underline: counters.u > 0
121+
});
122+
}
123+
}
124+
125+
return fragments;
126+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { PoHelperContentPipe } from './po-helper-content.pipe';
2+
3+
describe('PoHelperContentPipe', () => {
4+
let pipe: PoHelperContentPipe;
5+
6+
beforeEach(() => {
7+
pipe = new PoHelperContentPipe();
8+
});
9+
10+
it('should create an instance', () => {
11+
expect(pipe).toBeTruthy();
12+
});
13+
14+
it('should return empty array for null content', () => {
15+
expect(pipe.transform(null)).toEqual([]);
16+
});
17+
18+
it('should return empty array for undefined content', () => {
19+
expect(pipe.transform(undefined)).toEqual([]);
20+
});
21+
22+
it('should return empty array for empty string', () => {
23+
expect(pipe.transform('')).toEqual([]);
24+
});
25+
26+
it('should return single fragment for plain text', () => {
27+
const result = pipe.transform('Texto simples');
28+
expect(result).toEqual([{ text: 'Texto simples', bold: false, italic: false, underline: false }]);
29+
});
30+
31+
it('should parse bold tag', () => {
32+
const result = pipe.transform('Texto <b>negrito</b>');
33+
expect(result[1]).toEqual({ text: 'negrito', bold: true, italic: false, underline: false });
34+
});
35+
36+
it('should parse italic tag', () => {
37+
const result = pipe.transform('Texto <i>itálico</i>');
38+
expect(result[1]).toEqual({ text: 'itálico', bold: false, italic: true, underline: false });
39+
});
40+
41+
it('should parse underline tag', () => {
42+
const result = pipe.transform('Texto <u>sublinhado</u>');
43+
expect(result[1]).toEqual({ text: 'sublinhado', bold: false, italic: false, underline: true });
44+
});
45+
46+
it('should normalize strong to bold', () => {
47+
const result = pipe.transform('<strong>bold</strong>');
48+
expect(result[0]).toEqual({ text: 'bold', bold: true, italic: false, underline: false });
49+
});
50+
51+
it('should normalize em to italic', () => {
52+
const result = pipe.transform('<em>italic</em>');
53+
expect(result[0]).toEqual({ text: 'italic', bold: false, italic: true, underline: false });
54+
});
55+
56+
it('should sanitize script tags', () => {
57+
const result = pipe.transform('<script>alert("xss")</script>safe');
58+
const allText = result.map(f => f.text).join('');
59+
expect(allText).not.toContain('<script>');
60+
expect(allText).toContain('safe');
61+
});
62+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Pipe, PipeTransform } from '@angular/core';
2+
import { parseHelperContent, PoHelperTextFragment } from './po-helper-content-utils';
3+
4+
/**
5+
* @description
6+
*
7+
* Pipe que transforma o conteúdo do helper em fragmentos de texto formatados,
8+
* utilizando o Safe Parser para garantir segurança contra XSS.
9+
*
10+
* Uso interno do componente `po-helper`.
11+
*/
12+
@Pipe({
13+
name: 'poHelperContent',
14+
standalone: false
15+
})
16+
export class PoHelperContentPipe implements PipeTransform {
17+
transform(content: string): Array<PoHelperTextFragment> {
18+
return parseHelperContent(content);
19+
}
20+
}

0 commit comments

Comments
 (0)