blob: 9cc4c26464b7dc684aa2c5db67be3c48bdf4cbce [file] [log] [blame]
// Copyright (c) 2023, 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:dartdoc/src/model/indexable.dart';
import 'package:meta/meta.dart';
enum _MatchPosition {
isExactly,
startsWith,
contains;
int operator -(_MatchPosition other) => index - other.index;
}
class Index {
final List<IndexItem> index;
@visibleForTesting
Index(this.index);
factory Index.fromJson(String text) {
var jsonIndex = (jsonDecode(text) as List).cast<Map<String, dynamic>>();
return Index(jsonIndex.map(IndexItem.fromMap).toList());
}
List<IndexItem> find(String rawQuery) {
if (rawQuery.isEmpty) {
return [];
}
var query = rawQuery.toLowerCase();
var allMatches = <({IndexItem item, _MatchPosition matchPosition})>[];
for (var item in index) {
void score(_MatchPosition matchPosition) {
allMatches.add((item: item, matchPosition: matchPosition));
}
var lowerName = item.name.toLowerCase();
var lowerQualifiedName = item.qualifiedName.toLowerCase();
if (lowerName == query ||
lowerQualifiedName == query ||
lowerName == 'dart:$query') {
score(_MatchPosition.isExactly);
} else if (query.length > 1) {
if (lowerName.startsWith(query) ||
lowerQualifiedName.startsWith(query)) {
score(_MatchPosition.startsWith);
} else if (lowerName.contains(query) ||
lowerQualifiedName.contains(query)) {
score(_MatchPosition.contains);
}
}
}
allMatches.sort((a, b) {
// Exact match vs substring is king. If the user has typed the whole term
// they are searching for, but it isn't at the top, they cannot type any
// more to try and find it.
var comparison = a.matchPosition - b.matchPosition;
if (comparison != 0) {
return comparison;
}
// Prefer packages higher in the package order.
comparison = a.item.packageRank - b.item.packageRank;
if (comparison != 0) {
return comparison;
}
// Prefer top-level elements to library members to class (etc.) members.
comparison = a.item._scope - b.item._scope;
if (comparison != 0) {
return comparison;
}
// Prefer non-overrides to overrides.
comparison = a.item.overriddenDepth - b.item.overriddenDepth;
if (comparison != 0) {
return comparison;
}
// Prefer shorter names to longer ones.
return a.item.name.length - b.item.name.length;
});
return allMatches.map((match) => match.item).toList();
}
}
class IndexItem {
final String name;
final String qualifiedName;
final int packageRank;
final Kind kind;
final String? href;
final int overriddenDepth;
final String? desc;
final EnclosedBy? enclosedBy;
IndexItem._({
required this.name,
required this.qualifiedName,
required this.packageRank,
required this.kind,
required this.desc,
required this.href,
required this.overriddenDepth,
required this.enclosedBy,
});
// Example Map structure:
//
// ```dart
// {
// "name":"dartdoc",
// "qualifiedName":"dartdoc.Dartdoc",
// "href":"dartdoc/Dartdoc-class.html",
// "kind":1,
// "overriddenDepth":0,
// "packageRank":0
// ["enclosedBy":{"name":"dartdoc","kind":2}]
// }
// ```
factory IndexItem.fromMap(Map<String, dynamic> data) {
EnclosedBy? enclosedBy;
if (data['enclosedBy'] != null) {
final map = data['enclosedBy'] as Map<String, dynamic>;
enclosedBy = EnclosedBy._(
name: map['name'] as String,
kind: Kind.values[map['kind'] as int],
href: map['href'] as String);
}
return IndexItem._(
name: data['name'],
qualifiedName: data['qualifiedName'],
packageRank: data['packageRank'] as int,
href: data['href'],
kind: Kind.values[data['kind'] as int],
overriddenDepth: (data['overriddenDepth'] as int?) ?? 0,
desc: data['desc'],
enclosedBy: enclosedBy,
);
}
/// The "scope" of a search item which may affect ranking.
///
/// This is not the lexical scope of identifiers in Dart code, but similar in
/// a very loose sense.
int get _scope => switch (kind) {
// Library members.
Kind.class_ => 0,
Kind.enum_ => 0,
Kind.extension => 0,
Kind.extensionType => 0,
Kind.mixin => 0,
Kind.topLevelConstant => 0,
Kind.topLevelProperty => 0,
Kind.typedef => 0,
// Container members.
Kind.accessor => 1,
Kind.constant => 1,
Kind.constructor => 1,
Kind.function => 1,
Kind.method => 1,
Kind.property => 1,
// Root- and package-level items.
Kind.library => 2,
Kind.package => 2,
Kind.topic => 2,
// Others.
Kind.dynamic => 3,
Kind.never => 3,
Kind.parameter => 3,
Kind.prefix => 3,
Kind.sdk => 3,
Kind.typeParameter => 3,
};
}
class EnclosedBy {
final String name;
final Kind kind;
final String href;
// Built from JSON structure:
// ["enclosedBy":{"name":"Accessor","kind":"class","href":"link"}]
EnclosedBy._({required this.name, required this.kind, required this.href});
}