com.android.tools.lint.detector.api.TextFormat Maven / Gradle / Ivy
/*
* Copyright (C) 2014 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.android.tools.lint.detector.api;
import com.android.annotations.NonNull;
import com.android.utils.SdkUtils;
import com.android.utils.XmlUtils;
/**
* Lint error message, issue explanations and location descriptions
* are described in a {@link #RAW} format which looks similar to text
* but which can contain bold, symbols and links. These issues can
* also be converted to plain text and to HTML markup, using the
* {@link #convertTo(String, TextFormat)} method.
*
* @see Issue#getExplanation(TextFormat)
* @see Issue#getBriefDescription(TextFormat)
*/
public enum TextFormat {
/**
* Raw output format which is similar to text but allows some markup:
*
* - HTTP urls (http://...)
*
- Sentences immediately surrounded by * will be shown as bold.
*
- Sentences immediately surrounded by ` will be shown using monospace
* fonts
*
* Furthermore, newlines are converted to br's when converting newlines.
* Note: It does not insert {@code } tags around the fragment for HTML output.
*
* TODO: Consider switching to the restructured text format -
* http://docutils.sourceforge.net/docs/user/rst/quickstart.html
*/
RAW,
/**
* Plain text output
*/
TEXT,
/**
* HTML formatted output (note: does not include surrounding {@code } tags)
*/
HTML,
/**
* HTML formatted output (note: does not include surrounding {@code } tags).
* This is like {@link #HTML}, but it does not escape unicode characters with entities.
*
* (This is used for example in the IDE, where some partial HTML support in some
* label widgets support some HTML markup, but not numeric code character entities.)
*/
HTML_WITH_UNICODE;
/**
* Converts the given text to HTML
*
* @param text the text to format
* @return the corresponding text formatted as HTML
*/
@NonNull
public String toHtml(@NonNull String text) {
return convertTo(text, HTML);
}
/**
* Converts the given text to plain text
*
* @param text the tetx to format
* @return the corresponding text formatted as HTML
*/
@NonNull
public String toText(@NonNull String text) {
return convertTo(text, TEXT);
}
/**
* Converts the given message to the given format. Note that some
* conversions are lossy; e.g. once converting away from the raw format
* (which contains all the markup) you can't convert back to it.
* Note that you can convert to the format it's already in; that just
* returns the same string.
*
* @param message the message to convert
* @param to the format to convert to
* @return a converted message
*/
public String convertTo(@NonNull String message, @NonNull TextFormat to) {
if (this == to) {
return message;
}
switch (this) {
case RAW: {
switch (to) {
case RAW:
return message;
case TEXT:
case HTML:
case HTML_WITH_UNICODE:
return to.fromRaw(message);
}
}
case TEXT: {
switch (to) {
case TEXT:
case RAW:
return message;
case HTML:
case HTML_WITH_UNICODE:
return XmlUtils.toXmlTextValue(message);
}
}
case HTML: {
switch (to) {
case HTML:
return message;
case HTML_WITH_UNICODE:
return removeNumericEntities(message);
case RAW:
case TEXT: {
return to.fromHtml(message);
}
}
}
case HTML_WITH_UNICODE: {
switch (to) {
case HTML:
case HTML_WITH_UNICODE:
return message;
case RAW:
case TEXT: {
return to.fromHtml(message);
}
}
}
}
return message;
}
/** Converts to this output format from the given HTML-format text */
@NonNull
private String fromHtml(@NonNull String html) {
assert this == RAW || this == TEXT : this;
// Drop all tags; replace all entities, insert newlines
// (this won't do wrapping)
StringBuilder sb = new StringBuilder(html.length());
boolean inPre = false;
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '<') {
// Strip comments
if (html.startsWith("", i);
if (end == -1) {
break; // Unclosed comment
} else {
i = end + 2;
}
continue;
}
// Tags: scan forward to the end
int begin;
boolean isEndTag = false;
if (html.startsWith("", i)) {
begin = i + 2;
isEndTag = true;
} else {
begin = i + 1;
}
i = html.indexOf('>', i);
if (i == -1) {
// Unclosed tag
break;
}
int end = i;
if (html.charAt(i - 1) == '/') {
end--;
isEndTag = true;
}
// TODO: Handle
such that we don't collapse spaces and reformat there!
// (We do need to strip out tags and expand entities)
String tag = html.substring(begin, end).trim();
if (tag.equalsIgnoreCase("br")) {
sb.append('\n');
} else if (tag.equalsIgnoreCase("p") // Most common block tags
|| tag.equalsIgnoreCase("div")
|| tag.equalsIgnoreCase("pre")
|| tag.equalsIgnoreCase("blockquote")
|| tag.equalsIgnoreCase("dl")
|| tag.equalsIgnoreCase("dd")
|| tag.equalsIgnoreCase("dt")
|| tag.equalsIgnoreCase("ol")
|| tag.equalsIgnoreCase("ul")
|| tag.equalsIgnoreCase("li")
|| tag.length() == 2 && tag.startsWith("h")
&& Character.isDigit(tag.charAt(1))) {
// Block tag: ensure new line
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != '\n') {
sb.append('\n');
}
if (tag.equals("li") && !isEndTag) {
sb.append("* ");
}
if (tag.equalsIgnoreCase("pre")) {
inPre = !isEndTag;
}
}
} else if (c == '&') {
int end = html.indexOf(';', i);
if (end > i) {
String entity = html.substring(i, end + 1);
String s = XmlUtils.fromXmlAttributeValue(entity);
if (s.startsWith("&")) {
// Not an XML entity; for example,
// Sadly Guava's HtmlEscapes don't handle this either.
if (entity.equalsIgnoreCase(" ")) {
s = " ";
} else if (entity.startsWith("")) {
try {
int value = Integer.parseInt(entity.substring(2));
s = Character.toString((char)value);
} catch (NumberFormatException ignore) {
}
}
}
sb.append(s);
i = end;
} else {
sb.append(c);
}
} else if (Character.isWhitespace(c)) {
if (inPre) {
sb.append(c);
} else if (sb.length() == 0
|| !Character.isWhitespace(sb.charAt(sb.length() - 1))) {
sb.append(' ');
}
} else {
sb.append(c);
}
}
String s = sb.toString();
// Line-wrap
s = SdkUtils.wrap(s, 60, null);
return s;
}
private static final String HTTP_PREFIX = "http://";
/** Converts to this output format from the given raw-format text */
@NonNull
private String fromRaw(@NonNull String text) {
assert this == HTML || this == HTML_WITH_UNICODE || this == TEXT : this;
StringBuilder sb = new StringBuilder(3 * text.length() / 2);
boolean html = this == HTML || this == HTML_WITH_UNICODE;
boolean escapeUnicode = this == HTML;
char prev = 0;
int flushIndex = 0;
int n = text.length();
for (int i = 0; i < n; i++) {
char c = text.charAt(i);
if ((c == '*' || c == '`') && i < n - 1) {
// Scout ahead for range end
if (!Character.isLetterOrDigit(prev)
&& !Character.isWhitespace(text.charAt(i + 1))) {
// Found * or ` immediately before a letter, and not in the middle of a word
// Find end
int end = text.indexOf(c, i + 1);
if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i, escapeUnicode);
}
if (html) {
String tag = c == '*' ? "b" : "code";
sb.append('<').append(tag).append('>');
appendEscapedText(sb, text, html, i + 1, end, escapeUnicode);
sb.append('<').append('/').append(tag).append('>');
} else {
appendEscapedText(sb, text, html, i + 1, end, escapeUnicode);
}
flushIndex = end + 1;
i = flushIndex - 1; // -1: account for the i++ in the loop
}
}
} else if (html && c == 'h' && i < n - 1 && text.charAt(i + 1) == 't'
&& text.startsWith(HTTP_PREFIX, i) && !Character.isLetterOrDigit(prev)) {
// Find url end
int end = i + HTTP_PREFIX.length();
while (end < n) {
char d = text.charAt(end);
if (Character.isWhitespace(d)) {
break;
}
end++;
}
char last = text.charAt(end - 1);
if (last == '.' || last == ')' || last == '!') {
end--;
}
if (end > i + HTTP_PREFIX.length()) {
if (i > flushIndex) {
appendEscapedText(sb, text, html, flushIndex, i, escapeUnicode);
}
String url = text.substring(i, end);
sb.append("');
sb.append(url);
sb.append("");
flushIndex = end;
i = flushIndex - 1; // -1: account for the i++ in the loop
}
}
prev = c;
}
if (flushIndex < n) {
appendEscapedText(sb, text, html, flushIndex, n, escapeUnicode);
}
return sb.toString();
}
private static String removeNumericEntities(@NonNull String html) {
if (!html.contains("")) {
return html;
}
StringBuilder sb = new StringBuilder(html.length());
for (int i = 0, n = html.length(); i < n; i++) {
char c = html.charAt(i);
if (c == '&' && i < n - 1 && html.charAt(i + 1) == '#') {
int end = html.indexOf(';', i + 2);
if (end != -1) {
String decimal = html.substring(i + 2, end);
try {
c = (char)Integer.parseInt(decimal);
sb.append(c);
i = end;
continue;
} catch (NumberFormatException ignore) {
// fall through to not escape this
}
}
}
sb.append(c);
}
return sb.toString();
}
private static void appendEscapedText(@NonNull StringBuilder sb, @NonNull String text,
boolean html, int start, int end, boolean escapeUnicode) {
if (html) {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
if (c == '<') {
sb.append("<");
} else if (c == '&') {
sb.append("&");
} else if (c == '\n') {
sb.append("
\n");
} else {
if (c > 255 && escapeUnicode) {
sb.append("");
sb.append(Integer.toString(c));
sb.append(';');
} else if (c == '\u00a0') {
sb.append(" ");
} else {
sb.append(c);
}
}
}
} else {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
sb.append(c);
}
}
}
}