feat: offer a non-Mews web page as a simplified view or in the browser

This commit is contained in:
randogoth 2026-10-12 11:13:34 +03:00
parent cea66fbb42
commit 00e42c925c
24 changed files with 863 additions and 152 deletions

View file

@ -8,6 +8,7 @@ import 'package:http/http.dart' as http;
import '../../mews/atom.dart';
import '../../mews/document.dart';
import '../../mews/gemtext.dart';
import '../../mews/html.dart';
import '../../mews/parser.dart';
import '../../protocols/gemini.dart' as gemini;
import '../../protocols/spartan.dart' as spartan;
@ -153,12 +154,18 @@ class FetchHttpStatusError extends FetchResult {
}
class FetchParseError extends FetchResult {
const FetchParseError(this.message, this.source);
const FetchParseError(this.message, {this.simplified, this.declaresMews = false});
final String message;
/// The markup that failed to parse, for the "show page text" option.
final String source;
/// The page as the app can show it, for the simplified view, or null when
/// nothing legible came out of the markup.
final MewsPage? simplified;
/// Whether the markup declared the Mews conformance marker. A page that
/// claims to be Mews and still will not parse has a real markup error; one
/// that never claimed it is just an ordinary web page.
final bool declaresMews;
}
/// A Gemini or Spartan status carrying a message for the reader: a temporary
@ -304,7 +311,8 @@ class FetchHandler with Handler<FetchCommand, FetchResult> {
final newEtag = response.headers['etag'];
final newLastModified = response.headers['last-modified'];
return _parse(response.body, url, etag: newEtag, lastModified: newLastModified);
return _parse(response.body, url,
bytes: response.bodyBytes, etag: newEtag, lastModified: newLastModified);
}
/// The page could not be revalidated; use the cached copy when there is one.
@ -315,14 +323,32 @@ class FetchHandler with Handler<FetchCommand, FetchResult> {
etag: etag, lastModified: lastModified, notModified: true);
}
/// [bytes] are the undecoded body when there is one. The HTML conversion
/// reads the document's own charset from them, which an already-decoded
/// string has lost.
FetchResult _parse(String source, String url,
{String? etag, String? lastModified, bool notModified = false}) {
{List<int>? bytes, String? etag, String? lastModified, bool notModified = false}) {
try {
final page = parseMewsPage(source: source, url: url);
return FetchPageOk(
page: page, source: source, etag: etag, lastModified: lastModified, notModified: notModified);
} on MewsParseException catch (e) {
return FetchParseError(e.message, source);
return _parseFailure(e, source, bytes, url);
}
}
/// Markup the strict parse refused is converted for the simplified view, so
/// the reader is offered the structure Mews supports rather than nothing.
/// Converting here keeps the work off the main isolate.
FetchResult _parseFailure(
MewsParseException error, String source, List<int>? bytes, String url) {
try {
final converted = convertHtmlPage(url: url, source: source, bytes: bytes);
return FetchParseError(error.message,
simplified: converted.page, declaresMews: converted.declaresMews);
} on Exception {
// Nothing legible in it; the error page stands on its own.
return FetchParseError(error.message);
}
}
@ -497,7 +523,7 @@ class FetchHandler with Handler<FetchCommand, FetchResult> {
entries: parseAtomEntries(source: response.body, feedUrl: url),
);
} on MewsParseException catch (e) {
return FetchParseError(e.message, response.body);
return FetchParseError(e.message);
}
}
@ -532,7 +558,7 @@ class FetchHandler with Handler<FetchCommand, FetchResult> {
return const FetchNetworkError();
}
if (response.statusCode != 200) return FetchHttpStatusError(response.statusCode);
return _parse(response.body, url);
return _parse(response.body, url, bytes: response.bodyBytes);
}
@override

View file

@ -23,11 +23,15 @@ class LoadFailed extends BrowseError {
const LoadFailed();
}
/// A page the strict Mews parse refused. [simplified] is the same markup read
/// as far as the Mews subset reaches, for the reader who asks to see it, and is
/// null when nothing legible came out. [declaresMews] separates a page with a
/// real markup error from an ordinary web page.
class BrokenPage extends BrowseError {
const BrokenPage(this.source);
const BrokenPage({this.simplified, this.declaresMews = false});
/// The markup that failed to parse; "show page text" recovers its content.
final String source;
final MewsPage? simplified;
final bool declaresMews;
}
/// A protocol status with the site's own message for the reader: a temporary
@ -435,9 +439,9 @@ class BrowserController {
));
await _db.recordVisit(url, page.title);
}
case FetchParseError(:final source):
case FetchParseError(:final simplified, :final declaresMews):
currentPage.value = null;
error.value = BrokenPage(source);
error.value = BrokenPage(simplified: simplified, declaresMews: declaresMews);
case FetchProtocolError(:final message):
currentPage.value = null;
error.value = ProtocolFailure(message);