181 lines
4.9 KiB
Dart
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();
|
|
}
|
|
}
|