Index: pkg/dartdoc/search.dart |
diff --git a/pkg/dartdoc/search.dart b/pkg/dartdoc/search.dart |
new file mode 100644 |
index 0000000000000000000000000000000000000000..342ce6c807e88d5844d2973f1b32ca3ee42d0689 |
--- /dev/null |
+++ b/pkg/dartdoc/search.dart |
@@ -0,0 +1,218 @@ |
+// Copyright (c) 2012, 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. |
+ |
+/** |
+ * [SearchText] represent the search field text. The text is viewed in three |
+ * ways: [text] holds the original search text, used for performing |
+ * case-sensitive matches, [lowerCase] holds the lower-case search text, used |
+ * for performing case-insenstive matches, [camelCase] holds a camel-case |
+ * interpretation of the search text, used to order matches in camel-case. |
+ */ |
+class SearchText { |
+ final String text; |
+ final String lowerCase; |
+ final String camelCase; |
+ |
+ SearchText(String searchText) |
+ : text = searchText, |
+ lowerCase = searchText.toLowerCase(), |
+ camelCase = searchText.isEmpty() ? '' |
+ : '${searchText.substring(0, 1).toUpperCase()}' |
+ '${searchText.substring(1)}'; |
+ |
+ int get length() => text.length; |
+ |
+ bool isEmpty() => length == 0; |
+} |
+ |
+/** |
+ * [StringMatch] represents the case-insensitive matching of [searchText] as a |
+ * substring within a [text]. |
+ */ |
+class StringMatch { |
+ final SearchText searchText; |
+ final String text; |
+ final int matchOffset; |
+ final int matchEnd; |
+ |
+ StringMatch(this.searchText, |
+ this.text, this.matchOffset, this.matchEnd); |
+ |
+ /** |
+ * Returns the HTML representation of the match. |
+ */ |
+ String toHtml() { |
+ return '${text.substring(0, matchOffset)}' |
+ '<span class="drop-down-link-highlight">$matchText</span>' |
+ '${text.substring(matchEnd)}'; |
+ } |
+ |
+ String get matchText() => |
+ text.substring(matchOffset, matchEnd); |
+ |
+ /** |
+ * Is [:true:] iff [searchText] matches the full [text] case-sensitively. |
+ */ |
+ bool get isFullMatch() => text == searchText.text; |
+ |
+ /** |
+ * Is [:true:] iff [searchText] matches a substring of [text] |
+ * case-sensitively. |
+ */ |
+ bool get isExactMatch() => matchText == searchText.text; |
+ |
+ /** |
+ * Is [:true:] iff [searchText] matches a substring of [text] when |
+ * [searchText] is interpreted as camel case. |
+ */ |
+ bool get isCamelCaseMatch() => matchText == searchText.camelCase; |
+} |
+ |
+/** |
+ * [Result] represents a match of the search text on a library, type or member. |
+ */ |
+class Result { |
+ final StringMatch prefix; |
+ final StringMatch match; |
+ |
+ final String library; |
+ final String type; |
+ final String args; |
+ final String kind; |
+ final String url; |
+ |
+ TableRowElement row; |
+ |
+ Result(this.match, this.kind, this.url, |
+ [this.library, this.type, String args, this.prefix]) |
+ : this.args = args != null ? '<$args>' : ''; |
+ |
+ bool get isTopLevel() => prefix == null && type == null; |
+ |
+ void addRow(TableElement table) { |
+ if (row != null) return; |
+ |
+ clickHandler(Event event) { |
+ window.location.href = url; |
+ hideDropDown(); |
+ } |
+ |
+ row = table.insertRow(table.rows.length); |
+ row.classes.add('drop-down-link-tr'); |
+ row.on.mouseDown.add((event) => hideDropDownSuspend = true); |
+ row.on.click.add(clickHandler); |
+ row.on.mouseUp.add((event) => hideDropDownSuspend = false); |
+ var sb = new StringBuffer(); |
+ sb.add('<td class="drop-down-link-td">'); |
+ sb.add('<table class="drop-down-table"><tr><td colspan="2">'); |
+ if (kind == GETTER) { |
+ sb.add('get '); |
+ } else if (kind == SETTER) { |
+ sb.add('set '); |
+ } |
+ sb.add(match.toHtml()); |
+ if (kind == CLASS || kind == INTERFACE || kind == TYPEDEF) { |
+ sb.add(args); |
+ } else if (kind == CONSTRUCTOR || kind == METHOD) { |
+ sb.add('(...)'); |
+ } |
+ sb.add('</td></tr><tr><td class="drop-down-link-kind">'); |
+ sb.add(kindToString(kind)); |
+ if (prefix != null) { |
+ sb.add(' in '); |
+ sb.add(prefix.toHtml()); |
+ sb.add(args); |
+ } else if (type != null) { |
+ sb.add(' in '); |
+ sb.add(type); |
+ sb.add(args); |
+ } |
+ |
+ sb.add('</td><td class="drop-down-link-library">'); |
+ if (library != null) { |
+ sb.add('library $library'); |
+ } |
+ sb.add('</td></tr></table></td>'); |
+ row.innerHTML = sb.toString(); |
+ } |
+} |
+ |
+/** |
+ * Creates a [StringMatch] object for [text] if a substring matches |
+ * [searchText], or returns [: null :] if no match is found. |
+ */ |
+StringMatch obtainMatch(SearchText searchText, String text) { |
+ if (searchText.isEmpty()) { |
+ return new StringMatch(searchText, text, 0, 0); |
+ } |
+ int offset = text.toLowerCase().indexOf(searchText.lowerCase); |
+ if (offset != -1) { |
+ return new StringMatch(searchText, text, |
+ offset, offset + searchText.length); |
+ } |
+ return null; |
+} |
+ |
+/** |
+ * Compares [a] and [b], regarding [:true:] smaller than [:false:]. |
+ * |
+ * [:null:]-values are not handled. |
+ */ |
+int compareBools(bool a, bool b) { |
+ if (a == b) return 0; |
+ return a ? -1 : 1; |
+} |
+ |
+/** |
+ * Used to sort the search results heuristically to show the more relevant match |
+ * in the top of the dropdown. |
+ */ |
+int resultComparator(Result a, Result b) { |
+ // Favor top level entities. |
+ int result = compareBools(a.isTopLevel, b.isTopLevel); |
+ if (result != 0) return result; |
+ |
+ if (a.prefix != null && b.prefix != null) { |
+ // Favor full prefix matches. |
+ result = compareBools(a.prefix.isFullMatch, b.prefix.isFullMatch); |
+ if (result != 0) return result; |
+ } |
+ |
+ // Favor matches in the start. |
+ result = compareBools(a.match.matchOffset == 0, |
+ b.match.matchOffset == 0); |
+ if (result != 0) return result; |
+ |
+ // Favor matches to the end. For example, prefer 'cancel' over 'cancelable' |
+ result = compareBools(a.match.matchEnd == a.match.text.length, |
+ b.match.matchEnd == b.match.text.length); |
+ if (result != 0) return result; |
+ |
+ // Favor exact case-sensitive matches. |
+ result = compareBools(a.match.isExactMatch, b.match.isExactMatch); |
+ if (result != 0) return result; |
+ |
+ // Favor matches that do not break camel-case. |
+ result = compareBools(a.match.isCamelCaseMatch, b.match.isCamelCaseMatch); |
+ if (result != 0) return result; |
+ |
+ // Favor matches close to the begining. |
+ result = a.match.matchOffset.compareTo(b.match.matchOffset); |
+ if (result != 0) return result; |
+ |
+ if (a.type != null && b.type != null) { |
+ // Favor short type names over long. |
+ result = a.type.length.compareTo(b.type.length); |
+ if (result != 0) return result; |
+ |
+ // Sort type alphabetically. |
+ // TODO(4805): Use [:type.compareToIgnoreCase] when supported. |
+ result = a.type.toLowerCase().compareTo(b.type.toLowerCase()); |
+ if (result != 0) return result; |
+ } |
+ |
+ // Sort match alphabetically. |
+ // TODO(4805): Use [:text.compareToIgnoreCase] when supported. |
+ return a.match.text.toLowerCase().compareTo(b.match.text.toLowerCase()); |
+} |