diff options
author | Tor Norbye <tnorbye@google.com> | 2011-11-21 12:18:04 -0800 |
---|---|---|
committer | Tor Norbye <tnorbye@google.com> | 2011-11-23 13:16:32 -0800 |
commit | 1b765a82ab15dd2bd3d7bc7e43a7daf37a097e3f (patch) | |
tree | 0a7da811f5d438f05ff0be6faecde90c87b44f5c /lint | |
parent | 4f14da7e65c56041ea328e0e018316ec822307bc (diff) | |
download | sdk-1b765a82ab15dd2bd3d7bc7e43a7daf37a097e3f.zip sdk-1b765a82ab15dd2bd3d7bc7e43a7daf37a097e3f.tar.gz sdk-1b765a82ab15dd2bd3d7bc7e43a7daf37a097e3f.tar.bz2 |
Typography lint checker
This changeset adds a new typography detector. This looks at the
strings defined by the application and makes various suggestions to
make the text look better typographically, such as
- suggesting directional single and double quotes instead of straight
single or or double quotes
- suggesting typographical apostrophes instead of straight ones
- suggesting n-dashes instead of hyphens in number ranges
- suggesting m-dashes instead of --'s
- suggesting fractional characters instead of strings like 1/2
- suggesting the ellipsis character instead of "..."
- suggesting the copyright symbol instead of (c)
There is also an Eclipse quick fix for these issues.
Change-Id: If3912f9d34841faf069174f291911f73e6b78c28
Diffstat (limited to 'lint')
9 files changed, 641 insertions, 4 deletions
diff --git a/lint/cli/src/com/android/tools/lint/HtmlReporter.java b/lint/cli/src/com/android/tools/lint/HtmlReporter.java index 39e2f80..d00c406 100644 --- a/lint/cli/src/com/android/tools/lint/HtmlReporter.java +++ b/lint/cli/src/com/android/tools/lint/HtmlReporter.java @@ -352,7 +352,7 @@ class HtmlReporter extends Reporter { String explanation = issue.getExplanation(); explanation = explanation.replace("\n", "<br/>"); //$NON-NLS-1$ //$NON-NLS-2$ explanation = Main.wrap(explanation); - mWriter.write(explanation); + appendEscapedText(explanation); mWriter.write("\n</div>\n"); //$NON-NLS-1$; if (issue.getMoreInfo() != null) { mWriter.write("<div class=\"moreinfo\">"); //$NON-NLS-1$ @@ -505,7 +505,12 @@ class HtmlReporter extends Reporter { } else if (c == '&') { mWriter.write("&"); //$NON-NLS-1$ } else { - mWriter.write(c); + if (c > 255) { + mWriter.write("&#"); //$NON-NLS-1$ + mWriter.write(Integer.toString(c)); + } else { + mWriter.write(c); + } } } } diff --git a/lint/cli/src/com/android/tools/lint/Main.java b/lint/cli/src/com/android/tools/lint/Main.java index 5438712..2453ad5 100644 --- a/lint/cli/src/com/android/tools/lint/Main.java +++ b/lint/cli/src/com/android/tools/lint/Main.java @@ -168,10 +168,12 @@ public class Main extends LintClient { if (issue.getCategory().getName().startsWith(category) || issue.getCategory().getFullName().startsWith(category)) { describeIssue(issue); + System.out.println(); } } } else if (registry.isIssueId(id)) { describeIssue(registry.getIssue(id)); + System.out.println(); } else { System.err.println("Invalid id or category \"" + id + "\".\n"); displayValidIds(registry, System.err); @@ -479,7 +481,7 @@ public class Main extends LintClient { System.out.println(wrap(issue.getExplanation())); } if (issue.getMoreInfo() != null) { - System.out.println("\nMore information: " + issue.getMoreInfo()); + System.out.println("More information: " + issue.getMoreInfo()); } } diff --git a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java index 41eac96..88df665 100644 --- a/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java +++ b/lint/libs/lint_api/src/com/android/tools/lint/detector/api/Category.java @@ -136,4 +136,6 @@ public final class Category implements Comparable<Category> { // Sub categories /** Issues related to icons */ public static final Category ICONS = Category.create(USABILITY, "Icons", null, 7); + /** Issues related to typography */ + public static final Category TYPOGRAPHY = Category.create(USABILITY, "Typography", null, 8); } diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java index 946c1b6..d6b1d44 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/BuiltinIssueRegistry.java @@ -93,6 +93,11 @@ public class BuiltinIssueRegistry extends IssueRegistry { issues.add(IconDetector.DUPLICATES_NAMES); issues.add(IconDetector.DUPLICATES_CONFIGURATIONS); issues.add(IconDetector.ICON_NODPI); + issues.add(TypographyDetector.DASHES); + issues.add(TypographyDetector.QUOTES); + issues.add(TypographyDetector.FRACTIONS); + issues.add(TypographyDetector.ELLIPSIS); + issues.add(TypographyDetector.OTHER); issues.add(DetectMissingPrefix.MISSING_NAMESPACE); addCustomIssues(issues); diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java index 7fa2ae4..e1f923e 100644 --- a/lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/StateListDetector.java @@ -52,7 +52,7 @@ public class StateListDetector extends ResourceXmlDetector { /** Constructs a new {@link StateListDetector} */ public StateListDetector() { - }; + } @Override public boolean appliesTo(ResourceFolderType folderType) { diff --git a/lint/libs/lint_checks/src/com/android/tools/lint/checks/TypographyDetector.java b/lint/libs/lint_checks/src/com/android/tools/lint/checks/TypographyDetector.java new file mode 100644 index 0000000..01cb6bd --- /dev/null +++ b/lint/libs/lint_checks/src/com/android/tools/lint/checks/TypographyDetector.java @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2011 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.checks; + +import static com.android.tools.lint.detector.api.LintConstants.TAG_STRING; +import static com.android.tools.lint.detector.api.LintConstants.TAG_STRING_ARRAY; + +import com.android.annotations.VisibleForTesting; +import com.android.resources.ResourceFolderType; +import com.android.tools.lint.client.api.Configuration; +import com.android.tools.lint.detector.api.Category; +import com.android.tools.lint.detector.api.Context; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.ResourceXmlDetector; +import com.android.tools.lint.detector.api.Scope; +import com.android.tools.lint.detector.api.Severity; +import com.android.tools.lint.detector.api.Speed; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Checks for various typographical issues in string definitions. + */ +public class TypographyDetector extends ResourceXmlDetector { + /** Replace hyphens with dashes? */ + public static final Issue DASHES = Issue.create( + "TypographyDashes", //$NON-NLS-1$ + "Looks for usages of hyphens which can be replaced by n dash and m dash characters", + "The \"n dash\" (\u2013, \\u2013) and the \"m dash\" (\u2014, \\u2014) " + + "characters are used for ranges (n dash) and breaks (m dash). Using these " + + "instead of plain hyphens can make text easier to read and your application " + + "will look more polished.", + Category.TYPOGRAPHY, + 5, + Severity.WARNING, + TypographyDetector.class, + Scope.RESOURCE_FILE_SCOPE). + setMoreInfo("http://en.wikipedia.org/wiki/Dash"); //$NON-NLS-1$ + + /** Replace dumb quotes with smart quotes? */ + public static final Issue QUOTES = Issue.create( + "TypographyQuotes", //$NON-NLS-1$ + "Looks for straight quotes which can be replaced by curvy quotes", + "Straight single quotes and double quotes, when used as a pair, can be replaced " + + "by \"curvy quotes\" (or directional quotes). This can make the text more " + + "readable.\n" + + "\n" + + "Note that you should never use grave accents and apostrophes to quote, " + + "`like this'.\n" + + "\n" + + "(Also note that you should not use curvy quotes for code fragments.)", + Category.TYPOGRAPHY, + 5, + Severity.WARNING, + TypographyDetector.class, + Scope.RESOURCE_FILE_SCOPE). + setMoreInfo("http://en.wikipedia.org/wiki/Quotation_mark"). //$NON-NLS-1$ + // This feature is apparently controversial: recent apps have started using + // straight quotes to avoid inconsistencies. Disabled by default for now. + setEnabledByDefault(false); + + /** Replace fraction strings with fraction characters? */ + public static final Issue FRACTIONS = Issue.create( + "TypographyFractions", //$NON-NLS-1$ + "Looks for fraction strings which can be replaced with a fraction character", + "You can replace certain strings, such as 1/2, and 1/4, with dedicated " + + "characters for these, such as \u00BD (\\u00Bd) and \00BC (\\u00Bc). " + + "This can help make the text more readable.", + Category.TYPOGRAPHY, + 5, + Severity.WARNING, + TypographyDetector.class, + Scope.RESOURCE_FILE_SCOPE). + setMoreInfo("http://en.wikipedia.org/wiki/Number_Forms"); //$NON-NLS-1$ + + /** Replace ... with the ellipsis character? */ + public static final Issue ELLIPSIS = Issue.create( + "TypographyEllipsis", //$NON-NLS-1$ + "Looks for ellipsis strings (...) which can be replaced with an ellipsis character", + "You can replace the string \"...\" with a dedicated ellipsis character, " + + "ellipsis character (\u2026, \\u2026). This can help make the text more readable.", + Category.TYPOGRAPHY, + 5, + Severity.WARNING, + TypographyDetector.class, + Scope.RESOURCE_FILE_SCOPE). + setMoreInfo("http://en.wikipedia.org/wiki/Ellipsis"); //$NON-NLS-1$ + + /** The main issue discovered by this detector */ + public static final Issue OTHER = Issue.create( + "TypographyOther", //$NON-NLS-1$ + "Looks for miscellaneous typographical problems like replacing (c) with \u00A9", + "This check looks for miscellaneous typographical problems and offers replacement " + + "sequences that will make the text easier to read and your application more " + + "polished.", + Category.TYPOGRAPHY, + 3, + Severity.WARNING, + TypographyDetector.class, + Scope.RESOURCE_FILE_SCOPE); + + private static final String GRAVE_QUOTE_MESSAGE = + "Avoid quoting with grave accents; use apostrophes or better yet directional quotes instead"; + private static final String ELLIPSIS_MESSAGE = + "Replace \"...\" with ellipsis character (\u2026) ?"; + private static final String EN_DASH_MESSAGE = + "Replace \"-\" with an \"en dash\" character (\u2013, \\u2013) ?"; + private static final String EM_DASH_MESSAGE = + "Replace \"--\" with an \"em dash\" character (\u2014, \\u2014) ?"; + private static final String TYPOGRAPHIC_APOSTROPHE_MESSAGE = + "Replace apostrophe (') with typographic apostrophe (\u2019, \\u2019) ?"; + private static final String SINGLE_QUOTE_MESSAGE = + "Replace straight quotes ('') with directional quotes (\u2018\u2019, \\u2018 and \\u2019) ?"; + private static final String DBL_QUOTES_MESSAGE = + "Replace straight quotes (\") with directional quotes (\u201C\u201D, \\u201C and \\u201D) ?"; + private static final String COPYRIGHT_MESSAGE = + "Replace (c) with copyright symbol \u00A9 (\\u00A9) ?"; + + /** + * Pattern used to detect scenarios which can be replaced with n dashes: a + * numeric range with a hyphen in the middle (and possibly spaces) + */ + @VisibleForTesting + static final Pattern HYPHEN_RANGE_PATTERN = + Pattern.compile(".*(\\d+\\s*)-(\\s*\\d+).*"); //$NON-NLS-1$ + + /** + * Pattern used to detect scenarios where a grave accent mark is used + * to do ASCII quotations of the form `this'' or ``this'', which is frowned upon. + * This pattern tries to avoid falsely complaining about strings like + * "Type Option-` then 'Escape'." + */ + @VisibleForTesting + static final Pattern GRAVE_QUOTATION = + Pattern.compile("(^[^`]*`[^'`]+'[^']*$)|(^[^`]*``[^'`]+''[^']*$)"); //$NON-NLS-1$ + + /** + * Pattern used to detect common fractions, e.g. 1/2, 1/3, 2/3, 1/4, 3/4 and + * variations like 2 / 3, but not 11/22 and so on. + */ + @VisibleForTesting + static final Pattern FRACTION_PATTERN = + Pattern.compile(".*\\b([13])\\s*/\\s*([234])\\b.*"); //$NON-NLS-1$ + + /** + * Pattern used to detect single quote strings, such as 'hello', but + * not just quoted strings like 'Double quote: "', and not sentences + * where there are multiple apostrophes but not in a quoting context such + * as "Mind Your P's and Q's". + */ + @VisibleForTesting + static final Pattern SINGLE_QUOTE = + Pattern.compile(".*\\W*'[^']+'(\\W.*)?"); //$NON-NLS-1$ + + private static final String FRACTION_MESSAGE = + "Use fraction character %1$c (%2$s) instead of %3$s ?"; + + private boolean mCheckDashes; + private boolean mCheckQuotes; + private boolean mCheckFractions; + private boolean mCheckEllipsis; + private boolean mCheckMisc; + + /** Constructs a new {@link TypographyDetector} */ + public TypographyDetector() { + } + + @Override + public boolean appliesTo(ResourceFolderType folderType) { + return folderType == ResourceFolderType.VALUES; + } + + @Override + public Speed getSpeed() { + return Speed.FAST; + } + + @Override + public Collection<String> getApplicableElements() { + return Arrays.asList(new String[] { + TAG_STRING, + TAG_STRING_ARRAY + }); + } + + @Override + public void beforeCheckProject(Context context) { + Configuration configuration = context.configuration; + mCheckDashes = configuration.isEnabled(DASHES); + mCheckQuotes = configuration.isEnabled(QUOTES); + mCheckFractions = configuration.isEnabled(FRACTIONS); + mCheckEllipsis = configuration.isEnabled(ELLIPSIS); + mCheckMisc = configuration.isEnabled(OTHER); + } + + @Override + public void visitElement(Context context, Element element) { + NodeList childNodes = element.getChildNodes(); + for (int i = 0, n = childNodes.getLength(); i < n; i++) { + Node child = childNodes.item(i); + if (child.getNodeType() == Node.TEXT_NODE) { + String text = child.getNodeValue(); + checkText(context, element, text); + } else if (child.getNodeType() == Node.ELEMENT_NODE && + child.getParentNode().getNodeName().equals(TAG_STRING_ARRAY)) { + // String array item children + NodeList items = child.getChildNodes(); + for (int j = 0, m = items.getLength(); j < m; j++) { + Node item = items.item(j); + if (item.getNodeType() == Node.TEXT_NODE) { + String text = item.getNodeValue(); + checkText(context, child, text); + } + } + } + } + } + + private void checkText(Context context, Node element, String text) { + if (mCheckEllipsis) { + // Replace ... with ellipsis character? + int ellipsis = text.indexOf("..."); //$NON-NLS-1$ + if (ellipsis != -1 && !text.startsWith(".", ellipsis + 3)) { //$NON-NLS-1$ + context.client.report(context, ELLIPSIS, context.getLocation(element), + ELLIPSIS_MESSAGE, null); + } + } + + // Dashes + if (mCheckDashes) { + int hyphen = text.indexOf('-'); + if (hyphen != -1) { + // n dash + Matcher matcher = HYPHEN_RANGE_PATTERN.matcher(text); + if (matcher.matches()) { + // Make sure that if there is no space before digit there isn't + // one on the left either -- since we don't want to consider + // "1 2 -3" as a range from 2 to 3 + boolean isNegativeNumber = + !Character.isWhitespace(matcher.group(2).charAt(0)) && + Character.isWhitespace(matcher.group(1).charAt( + matcher.group(1).length() - 1)); + if (!isNegativeNumber) { + context.client.report(context, DASHES, context.getLocation(element), + EN_DASH_MESSAGE, + null); + } + } + + // m dash + int emdash = text.indexOf("--"); //$NON-NLS-1$ + // Don't suggest replacing -- or "--" with an m dash since these are sometimes + // used as digit marker strings + if (emdash > 1 && !text.startsWith("-", emdash + 2)) { //$NON-NLS-1$ + context.client.report(context, DASHES, context.getLocation(element), + EM_DASH_MESSAGE, null); + } + } + } + + if (mCheckQuotes) { + // Check for single quotes that can be replaced with directional quotes + int quoteStart = text.indexOf('\''); + if (quoteStart != -1) { + int quoteEnd = text.indexOf('\'', quoteStart + 1); + if (quoteEnd != -1 && quoteEnd > quoteStart + 1 + && (quoteEnd < text.length() -1 || quoteStart > 0) + && SINGLE_QUOTE.matcher(text).matches()) { + context.client.report(context, QUOTES, context.getLocation(element), + SINGLE_QUOTE_MESSAGE, null); + return; + } + + // Check for apostrophes that can be replaced by typographic apostrophes + if (quoteEnd == -1 && quoteStart > 0 + && Character.isLetterOrDigit(text.charAt(quoteStart - 1))) { + context.client.report(context, QUOTES, context.getLocation(element), + TYPOGRAPHIC_APOSTROPHE_MESSAGE, null); + return; + } + } + + // Check for double quotes that can be replaced by directional double quotes + quoteStart = text.indexOf('"'); + if (quoteStart != -1) { + int quoteEnd = text.indexOf('"', quoteStart + 1); + if (quoteEnd != -1 && quoteEnd > quoteStart + 1) { + if (quoteEnd < text.length() -1 || quoteStart > 0) { + context.client.report(context, QUOTES, context.getLocation(element), + DBL_QUOTES_MESSAGE, null); + return; + } + } + } + + // Check for grave accent quotations + if (text.indexOf('`') != -1 && GRAVE_QUOTATION.matcher(text).matches()) { + // Are we indenting ``like this'' or `this' ? If so, complain + context.client.report(context, QUOTES, context.getLocation(element), + GRAVE_QUOTE_MESSAGE, null); + return; + } + + // Consider suggesting other types of directional quotes, such as guillemets, in + // other languages? + // There are a lot of exceptions and special cases to be considered so + // this will need careful implementation and testing. + // See http://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks + } + + // Fraction symbols? + if (mCheckFractions && text.indexOf('/') != -1) { + Matcher matcher = FRACTION_PATTERN.matcher(text); + if (matcher.matches()) { + String top = matcher.group(1); // Numerator + String bottom = matcher.group(2); // Denominator + if (top.equals("1") && bottom.equals("2")) { //$NON-NLS-1$ //$NON-NLS-2$ + context.client.report(context, FRACTIONS, context.getLocation(element), + String.format(FRACTION_MESSAGE, '\u00BD', "\\u00BD", "1/2"), null); + } else if (top.equals("1") && bottom.equals("4")) { //$NON-NLS-1$ //$NON-NLS-2$ + context.client.report(context, FRACTIONS, context.getLocation(element), + String.format(FRACTION_MESSAGE, '\u00BC', "\\u00BC", "1/4"), null); + } else if (top.equals("3") && bottom.equals("4")) { //$NON-NLS-1$ //$NON-NLS-2$ + context.client.report(context, FRACTIONS, context.getLocation(element), + String.format(FRACTION_MESSAGE, '\u00BE', "\\u00BE", "3/4"), null); + } else if (top.equals("1") && bottom.equals("3")) { //$NON-NLS-1$ //$NON-NLS-2$ + context.client.report(context, FRACTIONS, context.getLocation(element), + String.format(FRACTION_MESSAGE, '\u2153', "\\u2153", "1/3"), null); + } else if (top.equals("2") && bottom.equals("3")) { //$NON-NLS-1$ //$NON-NLS-2$ + context.client.report(context, FRACTIONS, context.getLocation(element), + String.format(FRACTION_MESSAGE, '\u2154', "\\u2154", "2/3"), null); + } + } + } + + if (mCheckMisc) { + // Fix copyright symbol? + if (text.indexOf('(') != -1 + && (text.contains("(c)") || text.contains("(C)"))) { //$NON-NLS-1$ //$NON-NLS-2$ + // Suggest replacing with copyright symbol? + context.client.report(context, OTHER, context.getLocation(element), + COPYRIGHT_MESSAGE, null); + // Replace (R) and TM as well? There are unicode characters for these but they + // are probably not very common within Android app strings. + } + } + } + + /** + * An object describing a single edit to be made. The offset points to a + * location to start editing; the length is the number of characters to + * delete, and the replaceWith string points to a string to insert at the + * offset. Note that this can model not just replacement edits but deletions + * (empty replaceWith) and insertions (replace length = 0) too. + */ + public static class ReplaceEdit { + /** The offset of the edit */ + public final int offset; + /** The number of characters to delete at the offset */ + public final int length; + /** The characters to insert at the offset */ + public final String replaceWith; + + /** + * Creates a new replace edit + * + * @param offset the offset of the edit + * @param length the number of characters to delete at the offset + * @param replaceWith the characters to insert at the offset + */ + public ReplaceEdit(int offset, int length, String replaceWith) { + super(); + this.offset = offset; + this.length = length; + this.replaceWith = replaceWith; + } + } + + /** + * Returns a list of edits to be applied to fix the suggestion made by the + * given warning. The specific issue id and message should be the message + * provided by this detector in an earlier run. + * <p> + * This is intended to help tools implement automatic fixes of these + * warnings. The reason only the message and issue id can be provided + * instead of actual state passed in the data field to a reporter is that + * fix operation can be run much later than the lint is processed (for + * example, in a subsequent run of the IDE when only the warnings have been + * persisted), + * + * @param issueId the issue id, which should be the id for one of the + * typography issues + * @param message the actual error message, which should be a message + * provided by this detector + * @param textNode a text node which corresponds to the text node the + * warning operated on + * @return a list of edits, which is never null but could be empty. The + * offsets in the edit objects are relative to the text node. + */ + public static List<ReplaceEdit> getEdits(String issueId, String message, Node textNode) { + List<ReplaceEdit> edits = new ArrayList<ReplaceEdit>(); + String text = textNode.getNodeValue(); + if (message.equals(ELLIPSIS_MESSAGE)) { + int offset = text.indexOf("..."); //$NON-NLS-1$ + if (offset != -1) { + edits.add(new ReplaceEdit(offset, 3, "\u2026")); //$NON-NLS-1$ + } + } else if (message.equals(EN_DASH_MESSAGE)) { + int offset = text.indexOf('-'); + if (offset != -1) { + edits.add(new ReplaceEdit(offset, 1, "\u2013")); //$NON-NLS-1$ + } + } else if (message.equals(EM_DASH_MESSAGE)) { + int offset = text.indexOf("--"); //$NON-NLS-1$ + if (offset != -1) { + edits.add(new ReplaceEdit(offset, 2, "\u2014")); //$NON-NLS-1$ + } + } else if (message.equals(TYPOGRAPHIC_APOSTROPHE_MESSAGE)) { + int offset = text.indexOf('\''); + if (offset != -1) { + edits.add(new ReplaceEdit(offset, 1, "\u2019")); //$NON-NLS-1$ + } + } else if (message.equals(COPYRIGHT_MESSAGE)) { + int offset = text.indexOf("(c)"); //$NON-NLS-1$ + if (offset == -1) { + offset = text.indexOf("(C)"); //$NON-NLS-1$ + } + if (offset != -1) { + edits.add(new ReplaceEdit(offset, 3, "\u00A9")); //$NON-NLS-1$ + } + } else if (message.equals(SINGLE_QUOTE_MESSAGE)) { + int offset = text.indexOf('\''); + if (offset != -1) { + int endOffset = text.indexOf("'", offset + 1); //$NON-NLS-1$ + if (endOffset != -1) { + edits.add(new ReplaceEdit(offset, 1, "\u2018")); //$NON-NLS-1$ + edits.add(new ReplaceEdit(endOffset, 1, "\u2019")); //$NON-NLS-1$ + } + } + } else if (message.equals(DBL_QUOTES_MESSAGE)) { + int offset = text.indexOf('"'); + if (offset != -1) { + int endOffset = text.indexOf('"', offset + 1); + if (endOffset != -1) { + edits.add(new ReplaceEdit(offset, 1, "\u201C")); //$NON-NLS-1$ + edits.add(new ReplaceEdit(endOffset, 1, "\u201D")); //$NON-NLS-1$ + } + } + } else if (message.equals(GRAVE_QUOTE_MESSAGE)) { + int offset = text.indexOf('`'); + if (offset != -1) { + int endOffset = text.indexOf('\'', offset + 1); + if (endOffset != -1) { + edits.add(new ReplaceEdit(offset, 1, "\u2018")); //$NON-NLS-1$ + edits.add(new ReplaceEdit(endOffset, 1, "\u2019")); //$NON-NLS-1$ + } + } + } + + return edits; + } +} diff --git a/lint/libs/lint_checks/tests/.settings/org.eclipse.core.resources.prefs b/lint/libs/lint_checks/tests/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..204b512 --- /dev/null +++ b/lint/libs/lint_checks/tests/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +#Mon Nov 21 10:44:11 PST 2011 +eclipse.preferences.version=1 +encoding//src/com/android/tools/lint/checks/TypographyDetectorTest.java=UTF-8 diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TypographyDetectorTest.java b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TypographyDetectorTest.java new file mode 100644 index 0000000..f33f144 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/TypographyDetectorTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2011 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.checks; + +import static com.android.tools.lint.checks.TypographyDetector.FRACTION_PATTERN; +import static com.android.tools.lint.checks.TypographyDetector.GRAVE_QUOTATION; +import static com.android.tools.lint.checks.TypographyDetector.HYPHEN_RANGE_PATTERN; +import static com.android.tools.lint.checks.TypographyDetector.SINGLE_QUOTE; + +import com.android.tools.lint.detector.api.Detector; + +@SuppressWarnings("javadoc") +public class TypographyDetectorTest extends AbstractCheckTest { + @Override + protected Detector getDetector() { + return new TypographyDetector(); + } + + public void test() throws Exception { + assertEquals( + "typography.xml:10: Warning: Avoid quoting with grave accents; use apostrophes or better yet directional quotes instead\n" + + "typography.xml:11: Warning: Replace straight quotes ('') with directional quotes (‘’, \\u2018 and \\u2019) ?\n" + + "typography.xml:12: Warning: Use fraction character ½ (\\u00BD) instead of 1/2 ?\n" + + "typography.xml:13: Warning: Use fraction character ¼ (\\u00BC) instead of 1/4 ?\n" + + "typography.xml:15: Warning: Replace \"...\" with ellipsis character (…) ?\n" + + "typography.xml:17: Warning: Replace \"-\" with an \"en dash\" character (–, \\u2013) ?\n" + + "typography.xml:18: Warning: Replace \"-\" with an \"en dash\" character (–, \\u2013) ?\n" + + "typography.xml:20: Warning: Replace \"--\" with an \"em dash\" character (—, \\u2014) ?\n" + + "typography.xml:24: Warning: Replace \"-\" with an \"en dash\" character (–, \\u2013) ?\n" + + "typography.xml:25: Warning: Use fraction character ½ (\\u00BD) instead of 1/2 ?\n" + + "typography.xml:3: Warning: Replace straight quotes ('') with directional quotes (‘’, \\u2018 and \\u2019) ?\n" + + "typography.xml:5: Warning: Replace straight quotes (\") with directional quotes (“”, \\u201C and \\u201D) ?\n" + + "typography.xml:6: Warning: Replace straight quotes (\") with directional quotes (“”, \\u201C and \\u201D) ?\n" + + "typography.xml:7: Warning: Replace apostrophe (') with typographic apostrophe (’, \\u2019) ?\n" + + "typography.xml:8: Warning: Replace (c) with copyright symbol © (\\u00A9) ?\n" + + "typography.xml:9: Warning: Replace apostrophe (') with typographic apostrophe (’, \\u2019) ?", + + lintProject("res/values/typography.xml")); + } + + public void testSingleQuotesRange() { + assertTrue(SINGLE_QUOTE.matcher("Foo: 'bar'").matches()); + assertTrue(SINGLE_QUOTE.matcher("'Foo': bar").matches()); + assertTrue(SINGLE_QUOTE.matcher("\"'foo'\"").matches()); + assertTrue(SINGLE_QUOTE.matcher("\"'foo bar'\"").matches()); + + assertFalse(SINGLE_QUOTE.matcher("foo bar'").matches()); + assertFalse(SINGLE_QUOTE.matcher("Mind your P's and Q's").matches()); + + // This isn't asserted by the regexp: checked independently in + // the detector. The goal here is to assert that we need to + // have some text on either side of the quotes. + //assertFalse(SINGLE_QUOTE.matcher("'foo bar'").matches()); + } + + public void testGraveRegexp() { + assertTrue(GRAVE_QUOTATION.matcher("`a'").matches()); + assertTrue(GRAVE_QUOTATION.matcher(" `a' ").matches()); + assertTrue(GRAVE_QUOTATION.matcher(" ``a'' ").matches()); + assertFalse(GRAVE_QUOTATION.matcher("`a''").matches()); + } + + public void testFractionRegexp() { + assertTrue(FRACTION_PATTERN.matcher("fraction 1/2.").matches()); + assertTrue(FRACTION_PATTERN.matcher("1/2").matches()); + assertTrue(FRACTION_PATTERN.matcher("1/3").matches()); + assertTrue(FRACTION_PATTERN.matcher("1/4").matches()); + assertTrue(FRACTION_PATTERN.matcher("3/4").matches()); + assertTrue(FRACTION_PATTERN.matcher("1 / 2").matches()); + assertTrue(FRACTION_PATTERN.matcher("1 / 3").matches()); + assertTrue(FRACTION_PATTERN.matcher("1 / 4").matches()); + assertTrue(FRACTION_PATTERN.matcher("3 / 4").matches()); + + assertFalse(FRACTION_PATTERN.matcher("3 // 4").matches()); + assertFalse(FRACTION_PATTERN.matcher("11 / 2").matches()); + assertFalse(FRACTION_PATTERN.matcher("1 / 22").matches()); + } + + public void testNDashRegexp() { + assertTrue(HYPHEN_RANGE_PATTERN.matcher("3-4").matches()); + assertTrue(HYPHEN_RANGE_PATTERN.matcher("13- 14").matches()); + assertTrue(HYPHEN_RANGE_PATTERN.matcher("13 - 14").matches()); + assertTrue(HYPHEN_RANGE_PATTERN.matcher("The range is 13 - 14").matches()); + assertTrue(HYPHEN_RANGE_PATTERN.matcher("13 - 14.").matches()); + + assertFalse(HYPHEN_RANGE_PATTERN.matcher("13 - x").matches()); + assertFalse(HYPHEN_RANGE_PATTERN.matcher("x - 14").matches()); + assertFalse(HYPHEN_RANGE_PATTERN.matcher("x-y").matches()); + assertFalse(HYPHEN_RANGE_PATTERN.matcher("-y").matches()); + assertFalse(HYPHEN_RANGE_PATTERN.matcher("x-").matches()); + } +} diff --git a/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/typography.xml b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/typography.xml new file mode 100644 index 0000000..1dd4845 --- /dev/null +++ b/lint/libs/lint_checks/tests/src/com/android/tools/lint/checks/data/res/values/typography.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="home_title">Home 'Sample'</string> + <string name="show_all_apps">"All"</string> + <string name="show_all_apps2">Show "All"</string> + <string name="escaped">Skip \"All\"</string> + <string name="single">Android's</string> + <string name="copyright">(c) 2011</string> + <string name="badquotes1">`First'</string> + <string name="badquotes2">``second''</string> + <string name="notbadquotes">Type Option-` then 'Escape'</string> + <string name="fraction1">5 1/2 times</string> + <string name="fraction4">1/4 times</string> + <string name="notfraction">51/2 times, 1/20</string> + <string name="ellipsis">40 times...</string> + <string name="notellipsis">40 times.......</string> + <string name="ndash">For ages 3-5</string> + <string name="ndash2">Copyright 2007 - 2011</string> + <string name="nontndash">x-y</string> + <string name="mdash">Not found -- please try again</string> + <string name="nontndash">----</string> + <string name="notdirectional">A's and B's</string> + <string-array name="typography"> + <item>Ages 3-5</item> + <item>Age 5 1/2</item> + </string-array> + <string name="ndash">X Y Z: 10 10 -1</string> +</resources> + |