blob: bf57172443f75128d5d6eb8ec041c1b2bebe99c4 [file] [log] [blame]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:dartdoc/src/model/directives/categorization.dart';
import 'package:dartdoc/src/model/enclosed_element.dart';
import 'package:dartdoc/src/model/indexable.dart';
import 'package:dartdoc/src/model/model_element.dart';
String generateCategoryJson(Iterable<Categorization> categories, bool pretty) {
final indexItems = [
for (final categorization
in categories.sorted(_compareElementRepresentations))
<String, Object?>{
'qualifiedName': categorization.fullyQualifiedName,
'href': categorization.href,
// TODO(srawlins): Rename to 'kind'.
'type': categorization.kind.toString(),
if (categorization.hasCategoryNames)
'categories': categorization.categoryNames,
if (categorization.hasSubCategoryNames)
'subcategories': categorization.subCategoryNames,
if (categorization.hasImage) 'image': categorization.image,
if (categorization.hasSamples) 'samples': categorization.samples,
final encoder =
pretty ? const JsonEncoder.withIndent(' ') : const JsonEncoder();
return encoder.convert(indexItems.toList(growable: false));
/// Generates the text of the search index file (`index.json`) containing
/// [indexedItems] and [packageOrder].
/// Passing `pretty: true` will use a [JsonEncoder] with a single-space indent.
String generateSearchIndexJson(Iterable<Indexable> indexedElements,
{required List<String> packageOrder, required bool pretty}) {
final indexItems = [
for (final indexable
in indexedElements.sorted(_compareElementRepresentations))
'qualifiedName': indexable.fullyQualifiedName,
'href': indexable.href,
'kind': indexable.kind.index,
// TODO(srawlins): Only include this for [Inheritable] items.
'overriddenDepth': indexable.overriddenDepth,
if (indexable is ModelElement)
'packageRank': _packageRank(packageOrder, indexable),
if (indexable is ModelElement)
'desc': _removeHtmlTags(indexable.oneLineDoc),
if (indexable is EnclosedElement)
'enclosedBy': {
'kind': indexable.enclosingElement.kind.index,
'href': indexable.enclosingElement.href,
final encoder =
pretty ? const JsonEncoder.withIndent(' ') : const JsonEncoder();
return encoder.convert(indexItems);
/// The "package rank" of [element], given a [packageOrder].
/// Briefly, this is 10 times the element's package's position in the
/// [packageOrder], or 10 times the length of [packageOrder] if the element's
/// package is not listed in [packageOrder].
int _packageRank(List<String> packageOrder, ModelElement element) {
if (packageOrder.isEmpty) return 0;
var packageName =;
var index = packageOrder.indexOf(packageName);
if (index == -1) return packageOrder.length * 10;
if (packageName == 'Dart' &&
!_dartCoreLibraries.contains( {
// Non-"core" Dart SDK libraries should be ranked slightly lower than "core"
// Dart SDK libraries. The "core" Dart SDK libraries are the ones labeled as
// such at <>, which can be used in both VM and Web
// environments.
// Note we choose integers throughout this function (as opposed to adding
// 0.5 here) in order to facilitate a proper [Comparable] comparison.
return index * 10 + 5;
return index * 10;
/// The set of "core" Dart libraries, used to rank contained items above items
/// declared elsewhere in the Dart SDK.
const _dartCoreLibraries = {
// Plus the two core Flutter engine libraries.
String _removeHtmlTags(String? input) =>
input?.replaceAll(_htmlTagPattern, '') ?? '';
final _htmlTagPattern =
RegExp(r'<[^>]*>', multiLine: true, caseSensitive: true);
// Compares two elements, first by fully qualified name, then by kind.
int _compareElementRepresentations<T extends Indexable>(T a, T b) {
final value = compareNatural(a.fullyQualifiedName, b.fullyQualifiedName);
if (value == 0) {
return compareNatural(a.kind.toString(), b.kind.toString());
return value;