/TalkBack/src/com/google/android/marvin/talkback/formatter/WebContentHandler.java

http://eyes-free.googlecode.com/ · Java · 179 lines · 90 code · 28 blank · 61 comment · 31 complexity · fde9f6f76999310c2339375596219468 MD5 · raw file

  1. // Copyright 2010 Google Inc. All Rights Reserved.
  2. package com.google.android.marvin.talkback.formatter;
  3. import org.xml.sax.Attributes;
  4. import org.xml.sax.helpers.DefaultHandler;
  5. import java.util.Map;
  6. import java.util.Stack;
  7. /**
  8. * A handler for parsing simple HTML from Android WebView.
  9. *
  10. * @author credo@google.com (Tim Credo)
  11. */
  12. public class WebContentHandler extends DefaultHandler {
  13. /**
  14. * Maps input type attribute to element description.
  15. */
  16. private final Map<String, String> mInputTypeToDesc;
  17. /**
  18. * Maps ARIA role attribute to element description.
  19. */
  20. private final Map<String, String> mAriaRoleToDesc;
  21. /**
  22. * Map tags to element description.
  23. */
  24. private final Map<String, String> mTagToDesc;
  25. /**
  26. * A stack for storing post-order text generated by opening tags.
  27. */
  28. private Stack<String> mPostorderTextStack;
  29. /**
  30. * Builder for a string to be spoken based on parsed HTML.
  31. */
  32. private StringBuilder mOutputBuilder;
  33. /**
  34. * Initializes the handler with maps that provide descriptions for relevant
  35. * features in HTML.
  36. *
  37. * @param htmlInputMap A mapping from input types to text descriptions.
  38. * @param htmlRoleMap A mapping from ARIA roles to text descriptions.
  39. * @param htmlTagMap A mapping from common tags to text descriptions.
  40. */
  41. public WebContentHandler(Map<String, String> htmlInputMap, Map<String, String> htmlRoleMap,
  42. Map<String, String> htmlTagMap) {
  43. mInputTypeToDesc = htmlInputMap;
  44. mAriaRoleToDesc = htmlRoleMap;
  45. mTagToDesc = htmlTagMap;
  46. }
  47. @Override
  48. public void startDocument() {
  49. mOutputBuilder = new StringBuilder();
  50. mPostorderTextStack = new Stack<String>();
  51. }
  52. /**
  53. * Depending on the type of element, generate text describing its conceptual
  54. * value and role and add it to the output. The role text is spoken after
  55. * any content, so it is added to the stack to wait for the closing tag.
  56. */
  57. @Override
  58. public void startElement(String uri, String localName, String name, Attributes attributes) {
  59. fixWhiteSpace();
  60. final String ariaLabel = attributes.getValue("aria-label");
  61. final String alt = attributes.getValue("alt");
  62. final String title = attributes.getValue("title");
  63. if (ariaLabel != null) {
  64. mOutputBuilder.append(ariaLabel);
  65. } else if (alt != null) {
  66. mOutputBuilder.append(alt);
  67. } else if (title != null) {
  68. mOutputBuilder.append(title);
  69. }
  70. /*
  71. * Add role text to the stack so it appears after the content. If there
  72. * is no text we still need to push a blank string, since this will pop
  73. * when this element ends.
  74. */
  75. final String role = attributes.getValue("role");
  76. final String roleName = mAriaRoleToDesc.get(role);
  77. final String type = attributes.getValue("type");
  78. final String tagInfo = mTagToDesc.get(name.toLowerCase());
  79. if (roleName != null) {
  80. mPostorderTextStack.push(roleName);
  81. } else if (name.equalsIgnoreCase("input") && (type != null)) {
  82. final String typeInfo = mInputTypeToDesc.get(type.toLowerCase());
  83. if (typeInfo != null) {
  84. mPostorderTextStack.push(typeInfo);
  85. } else {
  86. mPostorderTextStack.push("");
  87. }
  88. } else if (tagInfo != null) {
  89. mPostorderTextStack.push(tagInfo);
  90. } else {
  91. mPostorderTextStack.push("");
  92. }
  93. /*
  94. * The value should be spoken as long as the element is not a form
  95. * element with a non-human-readable value.
  96. */
  97. final String value = attributes.getValue("value");
  98. if (value != null) {
  99. String elementType = name;
  100. if (name.equalsIgnoreCase("input") && (type != null)) {
  101. elementType = type;
  102. }
  103. if (!elementType.equalsIgnoreCase("checkbox") && !elementType.equalsIgnoreCase("radio")) {
  104. fixWhiteSpace();
  105. mOutputBuilder.append(value);
  106. }
  107. }
  108. }
  109. /**
  110. * Character data is passed directly to output.
  111. */
  112. @Override
  113. public void characters(char[] ch, int start, int length) {
  114. mOutputBuilder.append(ch, start, length);
  115. }
  116. /**
  117. * After the end of an element, get the post-order text from the stack and
  118. * add it to the output.
  119. */
  120. @Override
  121. public void endElement(String uri, String localName, String name) {
  122. final String postorderText = mPostorderTextStack.pop();
  123. if (postorderText.length() > 0) {
  124. fixWhiteSpace();
  125. }
  126. mOutputBuilder.append(postorderText);
  127. }
  128. /**
  129. * Ensure the output string has a character of whitespace before adding
  130. * another word.
  131. */
  132. public void fixWhiteSpace() {
  133. final int index = mOutputBuilder.length() - 1;
  134. if (index >= 0) {
  135. final char lastCharacter = mOutputBuilder.charAt(index);
  136. if (!Character.isWhitespace(lastCharacter)) {
  137. mOutputBuilder.append(" ");
  138. }
  139. }
  140. }
  141. /**
  142. * Get the processed string in mBuilder. Call this after parsing is done to
  143. * get the finished output.
  144. *
  145. * @return A string with HTML tags converted to descriptions suitable for
  146. * speaking.
  147. */
  148. public String getOutput() {
  149. return mOutputBuilder.toString();
  150. }
  151. }