199 lines
5.2 KiB
Dart
199 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;
|
||
|
}
|