summaryrefslogtreecommitdiffstats
path: root/java/com/google/gwtexpui/safehtml/client/HighlightSuggestOracle.java
blob: 758521fc0b31abdf454ff73ffefdf53154cd40a9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Copyright (C) 2009 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gwtexpui.safehtml.client;

import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.toList;

import com.google.gwt.user.client.ui.SuggestOracle;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * A suggestion oracle that tries to highlight the matched text.
 *
 * <p>Suggestions supplied by the implementation of {@link #onRequestSuggestions(Request, Callback)}
 * are modified to wrap all occurrences of the {@link
 * com.google.gwt.user.client.ui.SuggestOracle.Request#getQuery()} substring in HTML {@code
 * &lt;strong&gt;} tags, so they can be emphasized to the user.
 */
public abstract class HighlightSuggestOracle extends SuggestOracle {
  private static String escape(String ds) {
    return new SafeHtmlBuilder().append(ds).asString();
  }

  @Override
  public final boolean isDisplayStringHTML() {
    return true;
  }

  @Override
  public final void requestSuggestions(Request request, Callback cb) {
    onRequestSuggestions(
        request,
        new Callback() {
          @Override
          public void onSuggestionsReady(Request request, Response response) {
            final String qpat = getQueryPattern(request.getQuery());
            final boolean html = isHTML();
            final ArrayList<Suggestion> r = new ArrayList<>();
            for (Suggestion s : response.getSuggestions()) {
              r.add(new BoldSuggestion(qpat, s, html));
            }
            cb.onSuggestionsReady(request, new Response(r));
          }
        });
  }

  protected String getQueryPattern(String query) {
    return query;
  }

  /**
   * @return true if {@link
   *     com.google.gwt.user.client.ui.SuggestOracle.Suggestion#getDisplayString()} returns HTML;
   *     false if the text must be escaped before evaluating in an HTML like context.
   */
  protected boolean isHTML() {
    return false;
  }

  /** Compute the suggestions and return them for display. */
  protected abstract void onRequestSuggestions(Request request, Callback done);

  private static class BoldSuggestion implements Suggestion {
    private final Suggestion suggestion;
    private final String displayString;

    BoldSuggestion(String qstr, Suggestion s, boolean html) {
      suggestion = s;

      String ds = s.getDisplayString();
      if (!html) {
        ds = escape(ds);
      }

      if (qstr != null && !qstr.isEmpty()) {
        StringBuilder pattern = new StringBuilder();
        for (String qterm : splitQuery(qstr)) {
          qterm = escape(qterm);
          // We now surround qstr by <strong>. But the chosen approach is not too
          // smooth, if qstr is small (e.g.: "t") and this small qstr may occur in
          // escapes (e.g.: "Tim &lt;email@example.org&gt;"). Those escapes will
          // get <strong>-ed as well (e.g.: "&lt;" -> "&<strong>l</strong>t;"). But
          // as repairing those mangled escapes is easier than not mangling them in
          // the first place, we repair them afterwards.
          if (pattern.length() > 0) {
            pattern.append("|");
          }
          pattern.append(qterm);
        }

        ds = sgi(ds, "(" + pattern.toString() + ")", "<strong>$1</strong>");

        // Repairing <strong>-ed escapes.
        ds = sgi(ds, "(&[a-z]*)<strong>([a-z]*)</strong>([a-z]*;)", "$1$2$3");
      }

      displayString = ds;
    }

    /**
     * Split the query by whitespace and filter out query terms which are substrings of other query
     * terms.
     */
    private static List<String> splitQuery(String query) {
      List<String> queryTerms =
          Arrays.stream(query.split("\\s+")).sorted(comparing(String::length)).collect(toList());

      List<String> result = new ArrayList<>();
      for (String s : queryTerms) {
        boolean add = true;
        for (String queryTerm : result) {
          if (queryTerm.toLowerCase().contains(s.toLowerCase())) {
            add = false;
            break;
          }
        }
        if (add) {
          result.add(s);
        }
      }
      return result;
    }

    private static native String sgi(String inString, String pat, String newHtml)
        /*-{ return inString.replace(RegExp(pat, 'gi'), newHtml); }-*/ ;

    @Override
    public String getDisplayString() {
      return displayString;
    }

    @Override
    public String getReplacementString() {
      return suggestion.getReplacementString();
    }
  }
}