post_enrollment_app/lib/pages/helpers/aligned_html_content.dart
2026-07-06 09:55:31 +05:30

181 lines
4.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:html/dom.dart' as dom;
import 'package:html/parser.dart' as html_parser;
class AlignedHtmlContent extends StatelessWidget {
final String html;
final TextStyle bodyStyle;
final double markerWidth;
const AlignedHtmlContent({
super.key,
required this.html,
required this.bodyStyle,
this.markerWidth = 32,
});
@override
Widget build(BuildContext context) {
final document = html_parser.parse(html);
final body = document.body;
if (body == null) return const SizedBox.shrink();
return SizedBox(
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildNodes(body.nodes),
),
);
}
List<Widget> _buildNodes(List<dom.Node> nodes) {
final widgets = <Widget>[];
for (final node in nodes) {
widgets.addAll(_buildNode(node));
}
return widgets;
}
List<Widget> _buildNode(dom.Node node) {
if (node is dom.Text) {
final text = _normalizeWhitespace(node.text);
if (text.isEmpty) return [];
return [Text(text, style: bodyStyle)];
}
if (node is! dom.Element) return [];
switch (node.localName) {
case 'p':
final text = _normalizeWhitespace(node.text);
if (text.isEmpty) return [];
return [
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(text, style: bodyStyle),
),
];
case 'ol':
return [_buildOrderedList(node, 0)];
case 'ul':
return [_buildUnorderedList(node, 0)];
case 'br':
return [const SizedBox(height: 8)];
default:
return _buildNodes(node.nodes);
}
}
Widget _buildOrderedList(dom.Element ol, double indent) {
final items = ol.children.where((element) => element.localName == 'li');
final children = <Widget>[];
var index = 0;
for (final item in items) {
index += 1;
children.add(
Padding(
padding: EdgeInsets.only(left: indent, bottom: 8),
child: _buildOrderedListItem(item, index, indent),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
}
Widget _buildOrderedListItem(dom.Element li, int number, double indent) {
final leadingText = _liLeadingText(li);
final nestedLists =
li.children.where((element) => element.localName == 'ol' || element.localName == 'ul');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: markerWidth,
child: Text(
'$number.',
style: bodyStyle.copyWith(
fontFeatures: const [FontFeature.tabularFigures()],
),
),
),
Expanded(
child: Text(
leadingText,
style: bodyStyle,
),
),
],
),
for (final nested in nestedLists)
Padding(
padding: const EdgeInsets.only(top: 8),
child: nested.localName == 'ol'
? _buildOrderedList(nested, indent + markerWidth)
: _buildUnorderedList(nested, indent + markerWidth),
),
],
);
}
Widget _buildUnorderedList(dom.Element ul, double indent) {
final items = ul.children.where((element) => element.localName == 'li');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final item in items)
Padding(
padding: EdgeInsets.only(left: indent, bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: markerWidth,
child: Text(
'',
style: bodyStyle.copyWith(
color: const Color(0xFFE26728),
fontWeight: FontWeight.w600,
),
),
),
Expanded(
child: Text(
_normalizeWhitespace(item.text),
style: bodyStyle,
),
),
],
),
),
],
);
}
String _liLeadingText(dom.Element li) {
final buffer = StringBuffer();
for (final node in li.nodes) {
if (node is dom.Text) {
buffer.write(node.text);
} else if (node is dom.Element &&
node.localName != 'ol' &&
node.localName != 'ul') {
buffer.write(node.text);
}
}
return _normalizeWhitespace(buffer.toString());
}
String _normalizeWhitespace(String value) {
return value.replaceAll(RegExp(r'\s+'), ' ').trim();
}
}