You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

HtmlTextDecorator.java 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.source;
  21. import com.google.common.io.Closeables;
  22. import java.io.BufferedReader;
  23. import java.io.IOException;
  24. import java.io.StringReader;
  25. import java.util.Collection;
  26. import java.util.List;
  27. import javax.annotation.Nullable;
  28. import org.sonar.api.utils.log.Loggers;
  29. import static com.google.common.collect.Lists.newArrayList;
  30. class HtmlTextDecorator {
  31. static final char CR_END_OF_LINE = '\r';
  32. static final char LF_END_OF_LINE = '\n';
  33. static final char HTML_OPENING = '<';
  34. static final char HTML_CLOSING = '>';
  35. static final char AMPERSAND = '&';
  36. static final String ENCODED_HTML_OPENING = "&lt;";
  37. static final String ENCODED_HTML_CLOSING = "&gt;";
  38. static final String ENCODED_AMPERSAND = "&amp;";
  39. List<String> decorateTextWithHtml(String text, DecorationDataHolder decorationDataHolder) {
  40. return decorateTextWithHtml(text, decorationDataHolder, null, null);
  41. }
  42. List<String> decorateTextWithHtml(String text, DecorationDataHolder decorationDataHolder, @Nullable Integer from, @Nullable Integer to) {
  43. StringBuilder currentHtmlLine = new StringBuilder();
  44. List<String> decoratedHtmlLines = newArrayList();
  45. int currentLine = 1;
  46. BufferedReader stringBuffer = null;
  47. try {
  48. stringBuffer = new BufferedReader(new StringReader(text));
  49. CharactersReader charsReader = new CharactersReader(stringBuffer);
  50. while (charsReader.readNextChar()) {
  51. if (shouldStop(currentLine, to)) {
  52. break;
  53. }
  54. if (shouldStartNewLine(charsReader)) {
  55. if (canAddLine(currentLine, from)) {
  56. decoratedHtmlLines.add(currentHtmlLine.toString());
  57. }
  58. currentLine++;
  59. currentHtmlLine = new StringBuilder();
  60. }
  61. addCharToCurrentLine(charsReader, currentHtmlLine, decorationDataHolder);
  62. }
  63. closeCurrentSyntaxTags(charsReader, currentHtmlLine);
  64. if (shouldStartNewLine(charsReader)) {
  65. addLine(decoratedHtmlLines, currentHtmlLine.toString(), currentLine, from, to);
  66. currentLine++;
  67. addLine(decoratedHtmlLines, "", currentLine, from, to);
  68. } else if (currentHtmlLine.length() > 0) {
  69. addLine(decoratedHtmlLines, currentHtmlLine.toString(), currentLine, from, to);
  70. }
  71. } catch (IOException exception) {
  72. String errorMsg = "An exception occurred while highlighting the syntax of one of the project's files";
  73. Loggers.get(HtmlTextDecorator.class).error(errorMsg);
  74. throw new IllegalStateException(errorMsg, exception);
  75. } finally {
  76. Closeables.closeQuietly(stringBuffer);
  77. }
  78. return decoratedHtmlLines;
  79. }
  80. private void addCharToCurrentLine(CharactersReader charsReader, StringBuilder currentHtmlLine, DecorationDataHolder decorationDataHolder) {
  81. if (shouldStartNewLine(charsReader)) {
  82. if (shouldReopenPendingTags(charsReader)) {
  83. reopenCurrentSyntaxTags(charsReader, currentHtmlLine);
  84. }
  85. }
  86. int numberOfTagsToClose = getNumberOfTagsToClose(charsReader.getCurrentIndex(), decorationDataHolder);
  87. closeCompletedTags(charsReader, numberOfTagsToClose, currentHtmlLine);
  88. if (shouldClosePendingTags(charsReader)) {
  89. closeCurrentSyntaxTags(charsReader, currentHtmlLine);
  90. }
  91. Collection<String> tagsToOpen = getTagsToOpen(charsReader.getCurrentIndex(), decorationDataHolder);
  92. openNewTags(charsReader, tagsToOpen, currentHtmlLine);
  93. if (shouldAppendCharToHtmlOutput(charsReader)) {
  94. char currentChar = (char) charsReader.getCurrentValue();
  95. currentHtmlLine.append(normalize(currentChar));
  96. }
  97. }
  98. private static void addLine(List<String> decoratedHtmlLines, String line, int currentLine, @Nullable Integer from, @Nullable Integer to) {
  99. if (canAddLine(currentLine, from) && !shouldStop(currentLine, to)) {
  100. decoratedHtmlLines.add(line);
  101. }
  102. }
  103. private static boolean canAddLine(int currentLine, @Nullable Integer from) {
  104. return from == null || currentLine >= from;
  105. }
  106. private static boolean shouldStop(int currentLine, @Nullable Integer to) {
  107. return to != null && to < currentLine;
  108. }
  109. private static char[] normalize(char currentChar) {
  110. char[] normalizedChars;
  111. if (currentChar == HTML_OPENING) {
  112. normalizedChars = ENCODED_HTML_OPENING.toCharArray();
  113. } else if (currentChar == HTML_CLOSING) {
  114. normalizedChars = ENCODED_HTML_CLOSING.toCharArray();
  115. } else if (currentChar == AMPERSAND) {
  116. normalizedChars = ENCODED_AMPERSAND.toCharArray();
  117. } else {
  118. normalizedChars = new char[] {currentChar};
  119. }
  120. return normalizedChars;
  121. }
  122. private static boolean shouldAppendCharToHtmlOutput(CharactersReader charsReader) {
  123. return charsReader.getCurrentValue() != CR_END_OF_LINE && charsReader.getCurrentValue() != LF_END_OF_LINE;
  124. }
  125. private static int getNumberOfTagsToClose(int currentIndex, DecorationDataHolder dataHolder) {
  126. int numberOfTagsToClose = 0;
  127. while (currentIndex == dataHolder.getCurrentClosingTagOffset()) {
  128. numberOfTagsToClose++;
  129. dataHolder.nextClosingTagOffset();
  130. }
  131. return numberOfTagsToClose;
  132. }
  133. private static Collection<String> getTagsToOpen(int currentIndex, DecorationDataHolder dataHolder) {
  134. Collection<String> tagsToOpen = newArrayList();
  135. while (dataHolder.getCurrentOpeningTagEntry() != null && currentIndex == dataHolder.getCurrentOpeningTagEntry().getStartOffset()) {
  136. tagsToOpen.add(dataHolder.getCurrentOpeningTagEntry().getCssClass());
  137. dataHolder.nextOpeningTagEntry();
  138. }
  139. return tagsToOpen;
  140. }
  141. private static boolean shouldClosePendingTags(CharactersReader charactersReader) {
  142. return charactersReader.getCurrentValue() == CR_END_OF_LINE
  143. || (charactersReader.getCurrentValue() == LF_END_OF_LINE && charactersReader.getPreviousValue() != CR_END_OF_LINE)
  144. || (charactersReader.getCurrentValue() == CharactersReader.END_OF_STREAM && charactersReader.getPreviousValue() != LF_END_OF_LINE);
  145. }
  146. private static boolean shouldReopenPendingTags(CharactersReader charactersReader) {
  147. return (charactersReader.getPreviousValue() == LF_END_OF_LINE && charactersReader.getCurrentValue() != LF_END_OF_LINE)
  148. || (charactersReader.getPreviousValue() == CR_END_OF_LINE && charactersReader.getCurrentValue() != CR_END_OF_LINE
  149. && charactersReader.getCurrentValue() != LF_END_OF_LINE);
  150. }
  151. private static boolean shouldStartNewLine(CharactersReader charactersReader) {
  152. return charactersReader.getPreviousValue() == LF_END_OF_LINE
  153. || (charactersReader.getPreviousValue() == CR_END_OF_LINE && charactersReader.getCurrentValue() != LF_END_OF_LINE);
  154. }
  155. private static void closeCompletedTags(CharactersReader charactersReader, int numberOfTagsToClose,
  156. StringBuilder decoratedText) {
  157. for (int i = 0; i < numberOfTagsToClose; i++) {
  158. injectClosingHtml(decoratedText);
  159. charactersReader.removeLastOpenTag();
  160. }
  161. }
  162. private static void openNewTags(CharactersReader charactersReader, Collection<String> tagsToOpen,
  163. StringBuilder decoratedText) {
  164. for (String tagToOpen : tagsToOpen) {
  165. injectOpeningHtmlForRule(tagToOpen, decoratedText);
  166. charactersReader.registerOpenTag(tagToOpen);
  167. }
  168. }
  169. private static void closeCurrentSyntaxTags(CharactersReader charactersReader, StringBuilder decoratedText) {
  170. for (int i = 0; i < charactersReader.getOpenTags().size(); i++) {
  171. injectClosingHtml(decoratedText);
  172. }
  173. }
  174. private static void reopenCurrentSyntaxTags(CharactersReader charactersReader, StringBuilder decoratedText) {
  175. for (String tags : charactersReader.getOpenTags()) {
  176. injectOpeningHtmlForRule(tags, decoratedText);
  177. }
  178. }
  179. private static void injectOpeningHtmlForRule(String textType, StringBuilder decoratedText) {
  180. decoratedText.append("<span class=\"").append(textType).append("\">");
  181. }
  182. private static void injectClosingHtml(StringBuilder decoratedText) {
  183. decoratedText.append("</span>");
  184. }
  185. }