mirror of
https://github.com/immich-app/drift.git
synced 2026-09-30 13:22:57 +08:00
183 lines
4.8 KiB
Dart
183 lines
4.8 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:drift_website/src/common.dart';
|
|
import 'package:drift_website/src/components/inherited_page.dart';
|
|
import 'package:jaspr/server.dart';
|
|
import 'package:jaspr/src/server/async_build_owner.dart';
|
|
import 'package:jaspr_router/src/misc/inherited_router.dart';
|
|
import 'package:jaspr_content/jaspr_content.dart';
|
|
import 'package:jaspr_router/jaspr_router.dart';
|
|
import 'package:sqlite3/sqlite3.dart';
|
|
|
|
/// Creates a SQLite database file containing an FTS5 table indexing all content
|
|
/// in the documentation website.
|
|
void main() async {
|
|
final loader = driftWebsiteLoader();
|
|
final config = driftPageConfig(forSearchIndex: true);
|
|
final target = File('web/search.db');
|
|
if (target.existsSync()) {
|
|
target.deleteSync();
|
|
}
|
|
|
|
final routes = await loader.loadRoutes(config, true);
|
|
|
|
final db = sqlite3.openInMemory()
|
|
..createContentTable()
|
|
..execute('pragma page_size = 4096;');
|
|
for (final route in routes) {
|
|
if (route case final Route route) {
|
|
print('Has route ${route.path}, ${route.name}, ${route.title}');
|
|
|
|
final binding = _RenderTextBinding(currentUrl: route.path)
|
|
..attachRootComponent(_RenderSingleRoute(loader, route));
|
|
final (:title, :content) = await binding.render();
|
|
if (binding.hadError) {
|
|
print('Stopping due to error');
|
|
exit(1);
|
|
}
|
|
|
|
db.addPage(title, content, route.path);
|
|
}
|
|
}
|
|
|
|
print('Created FTS5 index for pages');
|
|
db.execute('VACUUM INTO ?', ['web/search.db']);
|
|
exit(0);
|
|
|
|
// TODO: It would be nice to also index documentation comments.
|
|
}
|
|
|
|
final class _RenderSingleRoute extends StatelessComponent {
|
|
final FilesystemLoader loader;
|
|
final Route route;
|
|
|
|
_RenderSingleRoute(this.loader, this.route);
|
|
|
|
@override
|
|
Component build(BuildContext context) {
|
|
final state = RouteState(
|
|
location: route.path,
|
|
subloc: '',
|
|
name: route.name,
|
|
);
|
|
return InheritedRouteState(
|
|
state: state,
|
|
child: InheritedPageResolver(
|
|
child: route.builder!(context, state),
|
|
loader: loader,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A jaspr render element implementation that just renders the inner text of
|
|
/// nodes without any escapes.
|
|
///
|
|
/// This is used to build content for the FTS5 index. We don't want to just
|
|
/// paste raw markdown into that index because the outputs of some components,
|
|
/// in particular of `<Snippets />`, should also be part of the index.
|
|
final class _RenderTextBinding extends AppBinding with ComponentsBinding {
|
|
@override
|
|
final String currentUrl;
|
|
|
|
var hadError = false;
|
|
|
|
_RenderTextBinding({required this.currentUrl});
|
|
|
|
@override
|
|
RenderObject createRootRenderObject() {
|
|
return RootMarkupRenderObject();
|
|
}
|
|
|
|
Future<({String title, String content})> render() async {
|
|
final root = rootElement!;
|
|
final completer = Completer();
|
|
root.binding.addPostFrameCallback(completer.complete);
|
|
await completer.future;
|
|
|
|
final buffer = StringBuffer();
|
|
var title = '';
|
|
|
|
void render(
|
|
MarkupRenderObject obj,
|
|
StringBuffer buffer,
|
|
bool strictFormatting,
|
|
) {
|
|
if (obj case MarkupRenderElement element) {
|
|
if (element.tag == 'title') {
|
|
title = (obj.children.single as MarkupRenderText).text;
|
|
} else if (element.tag == 'style') {
|
|
// Don't render inline styles
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (obj is MarkupRenderText) {
|
|
buffer.write(obj.text);
|
|
} else {
|
|
final strictFormattingHere =
|
|
strictFormatting ||
|
|
switch (obj) {
|
|
MarkupRenderElement(tag: 'span' || 'pre') => true,
|
|
_ => false,
|
|
};
|
|
|
|
for (final (i, child) in obj.children.indexed) {
|
|
if (!strictFormattingHere && i != 0) {
|
|
buffer.write(' ');
|
|
}
|
|
|
|
render(child, buffer, strictFormattingHere);
|
|
}
|
|
}
|
|
}
|
|
|
|
render(root.renderObject as MarkupRenderObject, buffer, false);
|
|
return (title: title, content: buffer.toString());
|
|
}
|
|
|
|
@override
|
|
BuildOwner createRootBuildOwner() {
|
|
return AsyncBuildOwner();
|
|
}
|
|
|
|
@override
|
|
bool get isClient => false;
|
|
|
|
@override
|
|
void reportBuildError(Element element, Object error, StackTrace stackTrace) {
|
|
hadError = true;
|
|
stderr.writeln(
|
|
'Error while building ${element.component.runtimeType}:\n$error\n\n$stackTrace',
|
|
);
|
|
}
|
|
|
|
@override
|
|
void scheduleFrame(VoidCallback frameCallback) {
|
|
throw UnsupportedError(
|
|
'Scheduling a frame is not supported on the server, and should never happen.',
|
|
);
|
|
}
|
|
}
|
|
|
|
extension on Database {
|
|
void createContentTable() {
|
|
execute('''
|
|
CREATE VIRTUAL TABLE content USING fts5(
|
|
title,
|
|
body,
|
|
path UNINDEXED
|
|
);
|
|
''');
|
|
}
|
|
|
|
void addPage(String title, String body, String path) {
|
|
execute('INSERT INTO content (title, body, path) VALUES (?, ?, ?)', [
|
|
title,
|
|
body,
|
|
path,
|
|
]);
|
|
}
|
|
}
|