buildSearchIndex function

Future<SearchIndexBuild> buildSearchIndex(
  1. Directory contentDir, {
  2. String routeOf(
    1. File file,
    2. String contentPath
    )?,
  3. String groupFor(
    1. String route
    )?,
  4. String titleFor(
    1. String route,
    2. Map<String, String> frontMatter
    )?,
  5. String descriptionFor(
    1. String route,
    2. Map<String, String> frontMatter
    )?,
  6. int compare(
    1. SearchDoc a,
    2. SearchDoc b
    )?,
  7. int maxSectionLength = 1200,
})

Walks contentDir for .md files and builds a SearchIndexBuild.

  • routeOf maps a file to its site route. Defaults to the convention content/a/b.md -> /a/b, with index.md mapping to its parent directory — see md.routeFor.
  • groupFor, titleFor and descriptionFor read a route (and, for the latter two, the page's own front matter) to fill in the fields a search result shows. All optional: a site that doesn't group pages, or has no per-page description, can skip them.
  • compare orders the final doc list. Left unset, docs stay in the order files were found (alphabetical by path). Passing one that reflects a site's own reading order — sidebar order, say — means that, all else equal in the ranking, earlier pages in that order win ties.
  • maxSectionLength caps how much of each section's body is kept, since the whole index downloads at once.

Implementation

Future<SearchIndexBuild> buildSearchIndex(
  Directory contentDir, {
  String Function(File file, String contentPath)? routeOf,
  String Function(String route)? groupFor,
  String Function(String route, Map<String, String> frontMatter)? titleFor,
  String Function(String route, Map<String, String> frontMatter)?
  descriptionFor,
  int Function(SearchDoc a, SearchDoc b)? compare,
  int maxSectionLength = 1200,
}) async {
  if (!contentDir.existsSync()) {
    throw ContentDirectoryNotFoundException(contentDir.path);
  }

  routeOf ??= (file, contentPath) => md.routeFor(file.path, contentPath);
  groupFor ??= (_) => '';
  titleFor ??= (route, frontMatter) => frontMatter['title'] ?? route;
  descriptionFor ??= (route, frontMatter) => frontMatter['description'] ?? '';

  final files =
      contentDir
          .listSync(recursive: true)
          .whereType<File>()
          .where((file) => file.path.endsWith('.md'))
          .toList()
        ..sort((a, b) => a.path.compareTo(b.path));

  final docs = <SearchDoc>[];
  for (final file in files) {
    final route = routeOf(file, contentDir.path);
    final raw = await file.readAsString();
    final (:frontMatter, :body) = md.splitFrontMatter(raw);

    docs.add(
      SearchDoc(
        url: route,
        title: titleFor(route, frontMatter),
        description: descriptionFor(route, frontMatter),
        group: groupFor(route),
        sections: [
          for (final section in md.sectionsOf(
            body,
            maxSectionLength: maxSectionLength,
          ))
            SearchSection(
              heading: section['h'] as String?,
              anchor: section['a'] as String?,
              body: section['b'] as String? ?? '',
            ),
        ],
      ),
    );
  }

  if (compare != null) docs.sort(compare);

  final json =
      '${const JsonEncoder.withIndent('  ').convert({
        'v': 1,
        'docs': [for (final doc in docs) doc.toJson()],
      })}\n';

  return SearchIndexBuild(docs: docs, json: json);
}