From dc9d62ed4dd34b021499c96fb758f6907ecad884 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Vilain Date: Mon, 8 Apr 2013 16:26:30 +0200 Subject: [PATCH] (SONAR-3893) Improve the highlighter API to not depend on sonar-channel and allow to work on multi-line tokens - Added CRLF support in HTML text decoration --- .../sonar/core/source/CharactersReader.java | 74 ++++++++++++++ .../sonar/core/source/HtmlTextWrapper.java | 97 +++++++++++-------- .../core/source/HtmlTextWrapperTest.java | 80 +++++++++++++-- 3 files changed, 201 insertions(+), 50 deletions(-) create mode 100644 sonar-core/src/main/java/org/sonar/core/source/CharactersReader.java diff --git a/sonar-core/src/main/java/org/sonar/core/source/CharactersReader.java b/sonar-core/src/main/java/org/sonar/core/source/CharactersReader.java new file mode 100644 index 00000000000..7cede8c815d --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/source/CharactersReader.java @@ -0,0 +1,74 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ + +package org.sonar.core.source; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Deque; + +public class CharactersReader { + + private static final int END_OF_STREAM = -1; + + private final BufferedReader stringBuffer; + private final Deque openTags; + + private int currentValue; + private int previousValue; + private int currentIndex = -1; + + public CharactersReader(BufferedReader stringBuffer) { + this.stringBuffer = stringBuffer; + this.openTags = new ArrayDeque(); + } + + public boolean readNextChar() throws IOException { + previousValue = currentValue; + currentValue = stringBuffer.read(); + currentIndex++; + return currentValue != END_OF_STREAM; + } + + public int getCurrentValue() { + return currentValue; + } + + public int getPreviousValue() { + return previousValue; + } + + public int getCurrentIndex() { + return currentIndex; + } + + public void registerOpenTag(String textType) { + openTags.push(textType); + } + + public void removeLastOpenTag() { + openTags.remove(); + } + + public Deque getOpenTags() { + return openTags; + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/source/HtmlTextWrapper.java b/sonar-core/src/main/java/org/sonar/core/source/HtmlTextWrapper.java index e5d33aae459..a3f8a30ad7f 100644 --- a/sonar-core/src/main/java/org/sonar/core/source/HtmlTextWrapper.java +++ b/sonar-core/src/main/java/org/sonar/core/source/HtmlTextWrapper.java @@ -22,6 +22,7 @@ package org.sonar.core.source; import com.google.common.base.Predicate; import com.google.common.collect.Collections2; +import org.slf4j.LoggerFactory; import org.sonar.api.scan.source.SyntaxHighlightingRule; import org.sonar.api.scan.source.SyntaxHighlightingRuleSet; @@ -30,22 +31,15 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.util.Collection; -import java.util.LinkedList; import java.util.List; -import java.util.Queue; public class HtmlTextWrapper { - private static final int END_OF_STREAM = -1; - private static final char END_OF_LINE = '\n'; private static final String OPEN_TABLE_LINE = ""; private static final String CLOSE_TABLE_LINE = ""; - private Queue currentOpenTags; - - public HtmlTextWrapper() { - currentOpenTags = new LinkedList(); - } + public static final char CR_END_OF_LINE = '\r'; + public static final char LF_END_OF_LINE = '\n'; public String wrapTextWithHtml(String text, SyntaxHighlightingRuleSet syntaxHighlighting) throws IOException { @@ -57,36 +51,34 @@ public class HtmlTextWrapper { try { stringBuffer = new BufferedReader(new StringReader(text)); - int currentCharValue = stringBuffer.read(); - int currentCharIndex = 0; - boolean isNewLine = true; + CharactersReader context = new CharactersReader(stringBuffer); - while(currentCharValue != END_OF_STREAM) { + while(context.readNextChar()) { - if(isNewLine) { + if(shouldStartNewLine(context)) { decoratedText.append(OPEN_TABLE_LINE); - reopenCurrentSyntaxTags(decoratedText); - isNewLine = false; + if(shouldReopenPendingTags(context)) { + reopenCurrentSyntaxTags(context, decoratedText); + } } - if(currentCharValue == END_OF_LINE) { - closeCurrentSyntaxTags(decoratedText); + Collection tagsToClose = + Collections2.filter(highlightingRules, new IndexRuleFilter(context.getCurrentIndex(), false)); + closeCompletedTags(context, tagsToClose, decoratedText); + + if(shouldClosePendingTags(context)) { + closeCurrentSyntaxTags(context, decoratedText); decoratedText.append(CLOSE_TABLE_LINE); - isNewLine = true; - } else { - Collection rulesMatchingCurrentIndex = - Collections2.filter(highlightingRules, new IndexRuleFilter(currentCharIndex)); - if(rulesMatchingCurrentIndex.size() > 0) { - injectHtml(currentCharIndex, rulesMatchingCurrentIndex, decoratedText); - } } - decoratedText.append((char)currentCharValue); - currentCharValue = stringBuffer.read(); - currentCharIndex++; + Collection tagsToOpen = + Collections2.filter(highlightingRules, new IndexRuleFilter(context.getCurrentIndex(), true)); + openNewTags(context, tagsToOpen, decoratedText); + + decoratedText.append((char)context.getCurrentValue()); } - } catch (Exception Ex) { - // + } catch (IOException exception) { + LoggerFactory.getLogger(HtmlTextWrapper.class).error(""); } finally { closeReaderSilently(stringBuffer); } @@ -94,31 +86,47 @@ public class HtmlTextWrapper { return decoratedText.toString(); } - private void injectHtml(int currentIndex, Collection rulesMatchingCurrentIndex, - StringBuilder decoratedText) { + public boolean shouldClosePendingTags(CharactersReader context) { + return context.getCurrentValue() == CR_END_OF_LINE + || (context.getCurrentValue() == LF_END_OF_LINE && context.getPreviousValue() != CR_END_OF_LINE); + } + + public boolean shouldReopenPendingTags(CharactersReader context) { + return context.getPreviousValue() == LF_END_OF_LINE && context.getCurrentValue() != LF_END_OF_LINE; + } + + public boolean shouldStartNewLine(CharactersReader context) { + return context.getPreviousValue() == LF_END_OF_LINE || context.getCurrentIndex() == 0; + } + + private void closeCompletedTags(CharactersReader context, Collection rulesMatchingCurrentIndex, + StringBuilder decoratedText) { for (SyntaxHighlightingRule syntaxHighlightingRule : rulesMatchingCurrentIndex) { - if(currentIndex == syntaxHighlightingRule.getEndPosition()) { + if(context.getCurrentIndex() == syntaxHighlightingRule.getEndPosition()) { injectClosingHtml(decoratedText); - currentOpenTags.remove(); + context.removeLastOpenTag(); } } + } + private void openNewTags(CharactersReader context, Collection rulesMatchingCurrentIndex, + StringBuilder decoratedText) { for (SyntaxHighlightingRule syntaxHighlightingRule : rulesMatchingCurrentIndex) { - if(currentIndex == syntaxHighlightingRule.getStartPosition()) { + if(context.getCurrentIndex() == syntaxHighlightingRule.getStartPosition()) { injectOpeningHtmlForRule(syntaxHighlightingRule.getTextType(), decoratedText); - currentOpenTags.add(syntaxHighlightingRule.getTextType()); + context.registerOpenTag(syntaxHighlightingRule.getTextType()); } } } - private void closeCurrentSyntaxTags(StringBuilder decoratedText) { - for (int i = 0; i < currentOpenTags.size(); i++) { + private void closeCurrentSyntaxTags(CharactersReader context, StringBuilder decoratedText) { + for (int i = 0; i < context.getOpenTags().size(); i++) { injectClosingHtml(decoratedText); } } - private void reopenCurrentSyntaxTags(StringBuilder decoratedText) { - for (String tags : currentOpenTags) { + private void reopenCurrentSyntaxTags(CharactersReader context, StringBuilder decoratedText) { + for (String tags : context.getOpenTags()) { injectOpeningHtmlForRule(tags, decoratedText); } } @@ -137,22 +145,25 @@ public class HtmlTextWrapper { reader.close(); } } catch (IOException e) { - // + LoggerFactory.getLogger(HtmlTextWrapper.class).error("Could not close "); } } private class IndexRuleFilter implements Predicate { private final int characterIndex; + private final boolean isNewCharRange; - public IndexRuleFilter(int charIndex) { + public IndexRuleFilter(int charIndex, boolean isNewCharRange) { this.characterIndex = charIndex; + this.isNewCharRange = isNewCharRange; } @Override public boolean apply(@Nullable SyntaxHighlightingRule syntaxHighlightingRule) { if(syntaxHighlightingRule != null) { - return characterIndex == syntaxHighlightingRule.getStartPosition() || characterIndex == syntaxHighlightingRule.getEndPosition(); + return (characterIndex == syntaxHighlightingRule.getStartPosition() && isNewCharRange) + || (characterIndex == syntaxHighlightingRule.getEndPosition() && !isNewCharRange); } return false; } diff --git a/sonar-core/src/test/java/org/sonar/core/source/HtmlTextWrapperTest.java b/sonar-core/src/test/java/org/sonar/core/source/HtmlTextWrapperTest.java index 51c1bffe9c4..6424583f9ba 100644 --- a/sonar-core/src/test/java/org/sonar/core/source/HtmlTextWrapperTest.java +++ b/sonar-core/src/test/java/org/sonar/core/source/HtmlTextWrapperTest.java @@ -25,11 +25,11 @@ import org.sonar.api.scan.source.HighlightableTextType; import org.sonar.api.scan.source.SyntaxHighlightingRuleSet; import static org.fest.assertions.Assertions.assertThat; +import static org.sonar.core.source.HtmlTextWrapper.CR_END_OF_LINE; +import static org.sonar.core.source.HtmlTextWrapper.LF_END_OF_LINE; public class HtmlTextWrapperTest { - private static final String NEW_LINE = "\n"; - @Test public void should_decorate_simple_character_range() throws Exception { @@ -51,18 +51,20 @@ public class HtmlTextWrapperTest { String secondCommentLine = " * Test"; String thirdCommentLine = " */"; - String blockComment = firstCommentLine + NEW_LINE + secondCommentLine + NEW_LINE + thirdCommentLine + NEW_LINE; + String blockComment = firstCommentLine + LF_END_OF_LINE + + secondCommentLine + LF_END_OF_LINE + + thirdCommentLine + LF_END_OF_LINE; - SyntaxHighlightingRuleSet syntaxHighlighting = new SyntaxHighlightingRuleSet.Builder() + SyntaxHighlightingRuleSet syntaxHighlighting = SyntaxHighlightingRuleSet.builder() .registerHighlightingRule(0, 14, HighlightableTextType.BLOCK_COMMENT).build(); HtmlTextWrapper htmlTextWrapper = new HtmlTextWrapper(); String htmlOutput = htmlTextWrapper.wrapTextWithHtml(blockComment, syntaxHighlighting); assertThat(htmlOutput).isEqualTo( - "" + firstCommentLine + "" + NEW_LINE + - "" + secondCommentLine + "" + NEW_LINE + - "" + thirdCommentLine + "" + NEW_LINE + "" + firstCommentLine + "" + LF_END_OF_LINE + + "" + secondCommentLine + "" + LF_END_OF_LINE + + "" + thirdCommentLine + "" + LF_END_OF_LINE ); } @@ -83,4 +85,68 @@ public class HtmlTextWrapperTest { assertThat(htmlOutput).isEqualTo( "public class MyClass implements MyInterface {\n"); } + + @Test + public void should_allow_multiple_levels_highlighting() throws Exception { + + String javaDocSample = + "/**" + LF_END_OF_LINE + + " * Creates a FormulaDecorator" + LF_END_OF_LINE + + " *" + LF_END_OF_LINE + + " * @param metric the metric should have an associated formula" + LF_END_OF_LINE + + " * " + LF_END_OF_LINE + + " * @throws IllegalArgumentException if no formula is associated to the metric" + LF_END_OF_LINE + + " */" + LF_END_OF_LINE; + + SyntaxHighlightingRuleSet syntaxHighlighting = SyntaxHighlightingRuleSet.builder() + .registerHighlightingRule(0, 184, HighlightableTextType.BLOCK_COMMENT) + .registerHighlightingRule(47, 53, HighlightableTextType.KEYWORD) + .build(); + + HtmlTextWrapper htmlTextWrapper = new HtmlTextWrapper(); + String htmlOutput = htmlTextWrapper.wrapTextWithHtml(javaDocSample, syntaxHighlighting); + + assertThat(htmlOutput).isEqualTo( + "/**" + LF_END_OF_LINE + + " * Creates a FormulaDecorator" + LF_END_OF_LINE + + " *" + LF_END_OF_LINE + + " * @param metric the metric should have an associated formula" + LF_END_OF_LINE + + " * " + LF_END_OF_LINE + + " * @throws IllegalArgumentException if no formula is associated to the metric" + LF_END_OF_LINE + + " */" + LF_END_OF_LINE + ); + } + + @Test + public void should_support_crlf_line_breaks() throws Exception { + + String crlfCodeSample = + "/**" + CR_END_OF_LINE + LF_END_OF_LINE + + "* @return metric generated by the decorator" + CR_END_OF_LINE + LF_END_OF_LINE + + "*/" + CR_END_OF_LINE + LF_END_OF_LINE + + "@DependedUpon" + CR_END_OF_LINE + LF_END_OF_LINE + + "public Metric generatesMetric() {" + CR_END_OF_LINE + LF_END_OF_LINE + + " return metric;" + CR_END_OF_LINE + LF_END_OF_LINE + + "}" + CR_END_OF_LINE + LF_END_OF_LINE; + + SyntaxHighlightingRuleSet syntaxHighlighting = SyntaxHighlightingRuleSet.builder() + .registerHighlightingRule(0, 52, HighlightableTextType.BLOCK_COMMENT) + .registerHighlightingRule(54, 67, HighlightableTextType.ANNOTATION) + .registerHighlightingRule(69, 75, HighlightableTextType.KEYWORD) + .registerHighlightingRule(106, 112, HighlightableTextType.KEYWORD) + .build(); + + HtmlTextWrapper htmlTextWrapper = new HtmlTextWrapper(); + String htmlOutput = htmlTextWrapper.wrapTextWithHtml(crlfCodeSample, syntaxHighlighting); + + assertThat(htmlOutput).isEqualTo( + "/**" + CR_END_OF_LINE + LF_END_OF_LINE + + "* @return metric generated by the decorator" + CR_END_OF_LINE + LF_END_OF_LINE + + "*/" + CR_END_OF_LINE + LF_END_OF_LINE + + "@DependedUpon" + CR_END_OF_LINE + LF_END_OF_LINE + + "public Metric generatesMetric() {" + CR_END_OF_LINE + LF_END_OF_LINE + + " return metric;" + CR_END_OF_LINE + LF_END_OF_LINE + + "}" + CR_END_OF_LINE + LF_END_OF_LINE + ); + } } -- 2.39.5