diff --git a/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart b/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart index 5415daf1a..07119143c 100644 --- a/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart @@ -6,6 +6,8 @@ import 'package:html/dom.dart'; class NormalizeLineHeightInStyleTransformer extends DomTransformer { const NormalizeLineHeightInStyleTransformer(); + static const _removableLineHeights = ['1px', '100%']; + @override Future process({ required Document document, @@ -13,19 +15,28 @@ class NormalizeLineHeightInStyleTransformer extends DomTransformer { Map? mapUrlDownloadCID, }) async { try { + final pattern = RegExp( + r'line-height\s*:\s*(?:' + _removableLineHeights.join('|') + r')\s*;?', + caseSensitive: false, + ); + final elements = document.querySelectorAll('[style*="line-height"]'); for (final element in elements) { final originalStyle = element.attributes['style']; if (originalStyle == null) continue; - final updatedStyle = originalStyle.replaceAllMapped( - RegExp(r'line-height\s*:\s*1px\s*;?', caseSensitive: false), - (match) => '', - ); + var updatedStyle = originalStyle.replaceAll(pattern, '').trim(); + + // Remove extra spaces (>=2 spaces → 1 space) + updatedStyle = updatedStyle.replaceAll(RegExp(r'\s{2,}'), ' '); if (updatedStyle != originalStyle) { - element.attributes['style'] = updatedStyle; + if (updatedStyle.isEmpty) { + element.attributes.remove('style'); + } else { + element.attributes['style'] = updatedStyle; + } } } } catch (e) { diff --git a/core/test/utils/normalize_line_height_in_style_transformer_test.dart b/core/test/utils/normalize_line_height_in_style_transformer_test.dart new file mode 100644 index 000000000..29f38773d --- /dev/null +++ b/core/test/utils/normalize_line_height_in_style_transformer_test.dart @@ -0,0 +1,108 @@ +import 'package:core/presentation/utils/html_transformer/dom/normalize_line_height_in_style_transformer.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:html/parser.dart' show parse; +import 'package:html/dom.dart'; +import 'package:core/data/network/dio_client.dart'; +import 'package:mockito/annotations.dart'; + +import 'normalize_line_height_in_style_transformer_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('NormalizeLineHeightInStyleTransformer', () { + const transformer = NormalizeLineHeightInStyleTransformer(); + final dioClient = MockDioClient(); + + Future run(String html) async { + final doc = parse(html); + await transformer.process(document: doc, dioClient: dioClient); + return doc; + } + + test('Should removes line-height:1px', () async { + final doc = await run('

Hello

'); + expect(doc.querySelector('p')!.attributes['style'], 'color:red;'); + }); + + test('Should removes line-height:100%', () async { + final doc = await run( + '

Hi

', + ); + expect(doc.querySelector('p')!.attributes['style'], 'font-size:14px;'); + }); + + test('Should keeps other line-height values', () async { + final doc = await run( + '

Hi

', + ); + expect( + doc.querySelector('p')!.attributes['style'], + 'line-height:150%; font-weight:bold;', + ); + }); + + test('Should removes style attribute if only line-height existed', + () async { + final doc = await run('
'); + expect( + doc.querySelector('div')!.attributes.containsKey('style'), + isFalse, + ); + }); + + test('Should handles missing style gracefully', () async { + final doc = await run('No style here'); + expect( + doc.querySelector('span')!.attributes.containsKey('style'), + isFalse, + ); + }); + + test('Should removes multiple unwanted line-heights', () async { + final doc = await run( + '

', + ); + expect( + doc.querySelector('p')!.attributes['style'], + 'color:blue; font-size:12px;', + ); + }); + + test('Should removes line-height even with irregular spacing', () async { + final doc = await run( + '

', + ); + expect(doc.querySelector('p')!.attributes['style'], 'color: green;'); + }); + + test('Should keep line-height:auto', () async { + final doc = await run( + '

', + ); + expect( + doc.querySelector('p')!.attributes['style'], + 'line-height:auto; font-size:16px;', + ); + }); + + test('Should not touch unrelated CSS properties', () async { + final doc = await run( + '

', + ); + expect( + doc.querySelector('p')!.attributes['style'], + 'margin:0; padding:5px;', + ); + }); + + test('Should trim trailing semicolon after removal', () async { + final doc = await run( + '

', + ); + expect( + doc.querySelector('p')!.attributes.containsKey('style'), + isFalse, + ); + }); + }); +}