voyagehandbook/lib/util/render.dart
2023-03-24 23:48:55 +01:00

198 lines
5.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:html/parser.dart';
import 'package:html/dom.dart' as dom;
import 'package:url_launcher/url_launcher.dart';
import 'package:voyagehandbook/api/classes.dart';
import 'package:voyagehandbook/util/styles.dart';
final _ignoredTags = ["style", "script"];
/// Used to create [TextSpan] from raw HTML from WM API
List<Widget> renderFromPageHTML(RawPage page) {
var out = <Widget>[
const SizedBox(
height: 10,
),
Text(
page.title,
textAlign: TextAlign.center,
style: PageStyles.h1,
),
const SizedBox(
height: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("From"),
const SizedBox(
width: 5,
),
GestureDetector(
onTap: () => launchUrl(
Uri.parse("https://en.wikivoyage.org/wiki/${page.key}"),
mode: LaunchMode.externalApplication),
child: const Text(
"WikiVoyage",
style: TextStyle(
decoration: TextDecoration.underline,
fontWeight: FontWeight.bold,
),
),
),
],
),
Row(
children: [
const Text("under"),
const SizedBox(
width: 5,
),
GestureDetector(
onTap: () => launchUrl(Uri.parse(page.license.url),
mode: LaunchMode.externalApplication),
child: Text(
page.license.title,
style: const TextStyle(
decoration: TextDecoration.underline,
),
),
)
],
)
];
var document = parse(page.html);
var sections = document.body!.getElementsByTagName("section");
for (var sec in sections) {
if (sec.localName == "section") {
out.addAll(_renderSection(sec));
}
}
return out;
}
List<Widget> _renderSection(dom.Element sec) {
var out = <Widget>[];
// Get Section Title
var headings = sec.children
.where((element) => ["h2", "h3", "h4", "h5"].contains(element.localName));
var sectionTitle = (headings.isNotEmpty) ? headings.first : null;
if (sectionTitle != null) {
switch (sectionTitle.localName) {
case "h2":
out.add(
const SizedBox(
height: 10,
),
);
out.add(Text(sectionTitle.text, style: PageStyles.h2));
break;
case "h3":
out.add(
Align(
alignment: Alignment.centerLeft,
child: Text(
sectionTitle.text,
style: PageStyles.h3,
),
),
);
break;
case "h4":
out.add(
Align(
alignment: Alignment.centerLeft,
child: Text(sectionTitle.text, style: PageStyles.h4),
),
);
break;
case "h5":
out.add(
Align(
alignment: Alignment.centerLeft,
child: Text(sectionTitle.text, style: PageStyles.h5),
),
);
break;
default:
out.add(
Align(
alignment: Alignment.centerLeft,
child: Text(sectionTitle.text),
),
);
break;
}
// out.add(
// const SizedBox(
// height: 5,
// ),
// );
}
// create TextSpans from text content
for (var element in sec.children) {
// Go through all section's children
for (var t in _ignoredTags) {
var ignored = element.getElementsByTagName(t);
if (ignored.isNotEmpty) {
// Remove ignored tags
for (var element in ignored) {
element.remove();
}
}
}
switch (element.localName) {
case "p":
case "link":
var paraSpans = <TextSpan>[];
var input = element.innerHtml
.replaceAll(RegExp(r"<(?!(?:\/b)|(?:\/i)|b|i).+?>"), "")
.replaceAll("\\n", "\r");
var noFormatting =
input.split(RegExp(r"(?:<b .+?>.+?<\/b>)|<i .+?>.+?<\/i>"));
var needToFormat = RegExp(r"(?:<b .+?>.+?<\/b>)|<i .+?>.+?<\/i>")
.allMatches(input)
.toList();
for (var s in needToFormat) {
paraSpans.add(TextSpan(
text: noFormatting[
needToFormat.indexOf(s)])); // add text before styled
var raw = s.group(0)!;
paraSpans.add(
TextSpan(
text: RegExp(r">(.+?)<").firstMatch(raw)!.group(1),
style: TextStyle(
fontWeight: (raw.contains("<b")) ? FontWeight.bold : null,
fontStyle: (raw.contains("<i")) ? FontStyle.italic : null,
),
),
); // add styled
}
paraSpans.add(TextSpan(text: noFormatting.last)); // add last
out.add(RichText(
text: TextSpan(children: paraSpans),
textAlign: TextAlign.justify,
)); // add paragraph spans as single rich text
out.add(
const SizedBox(
height: 5,
),
); // space paragraphs
break;
case "section":
out.addAll(_renderSection(element));
break;
default:
break;
}
}
out.add(
const SizedBox(
height: 5,
),
);
return out;
}