Handle bare CR in the middle of a line

Prettify converts a bare CR in the middle of a line of text into a
<br/>, which we later convert back into an LF.  This skews all of the
line numbers through the rest of the file, resulting in a confusing
situation (due to the unexpected LF) as we try to map the formatted
HTML back into the SparseFileContent object we use for rendering.

Highlight bare CRs as spaces with whitespace error spans around them,
before we feed the text into prettify.  This ensures we won't get
back more lines than we fed in, so our formatting is still correct.

Change-Id: Ib2dc8b024a2211601a588fd0552c4963d5b6e028
Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
index 346688e..e14063a 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
@@ -22,4 +22,5 @@
 
   String wseTabAfterSpace();
   String wseTrailingSpace();
+  String wseBareCR();
 }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
index 5cb864b..d440c65 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
@@ -1,2 +1,3 @@
 wseTabAfterSpace=Whitespace error: Tab after space
 wseTrailingSpace=Whitespace error: Trailing space at end of line
+wseBareCR=Whitespace error: CR without LF
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
index c5a8b27..4406477 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
@@ -314,6 +314,11 @@
         b.append('\n');
       }
       html = b;
+
+      final String r = "<span class=\"wse\"" //
+          + " title=\"" + PrettifyConstants.C.wseBareCR() + "\"" //
+          + ">&nbsp;</span>$1";
+      html = html.replaceAll("\r([^\n])", r);
     }
 
     if (settings.isShowWhiteSpaceErrors()) {
@@ -363,7 +368,7 @@
       if (cmp < 0) {
         // index occurs before the edit. This is a line of context.
         //
-        buf.append(src.get(index));
+        appendShowBareCR(buf, src.get(index), true);
         buf.append('\n');
         continue;
       }
@@ -378,10 +383,10 @@
           lastIdx = 0;
         }
 
-        final String line = src.get(index) + "\n";
+        String line = src.get(index) + "\n";
         for (int c = 0; c < line.length();) {
           if (charEdits.size() <= lastIdx) {
-            buf.append(line.substring(c));
+            appendShowBareCR(buf, line.substring(c), false);
             break;
           }
 
@@ -396,7 +401,8 @@
             final int cmnLen = Math.min(b, line.length());
             buf.openSpan();
             buf.setStyleName("wdc");
-            buf.append(line.substring(c, cmnLen));
+            appendShowBareCR(buf, line.substring(c, cmnLen), //
+                cmnLen == line.length() - 1);
             buf.closeSpan();
             c = cmnLen;
           }
@@ -405,7 +411,8 @@
           if (c < e && c < modLen) {
             buf.openSpan();
             buf.setStyleName(side.getStyleName());
-            buf.append(line.substring(c, modLen));
+            appendShowBareCR(buf, line.substring(c, modLen), //
+                modLen == line.length() - 1);
             buf.closeSpan();
             if (modLen == line.length()) {
               trailingEdits.add(index);
@@ -420,13 +427,41 @@
         lastPos += line.length();
 
       } else {
-        buf.append(src.get(index));
+        appendShowBareCR(buf, src.get(index), true);
         buf.append('\n');
       }
     }
     return buf;
   }
 
+  private void appendShowBareCR(SafeHtmlBuilder buf, String src, boolean end) {
+    while (!src.isEmpty()) {
+      int cr = src.indexOf('\r');
+      if (cr < 0) {
+        buf.append(src);
+        return;
+
+      } else if (end) {
+        if (cr == src.length() - 1) {
+          buf.append(src.substring(0, cr));
+          return;
+        }
+      } else if (cr == src.length() - 2 && src.charAt(cr + 1) == '\n') {
+        buf.append(src.substring(0, cr));
+        buf.append('\n');
+        return;
+      }
+
+      buf.append(src.substring(0, cr));
+      buf.openSpan();
+      buf.setStyleName("wse");
+      buf.setAttribute("title", PrettifyConstants.C.wseBareCR());
+      buf.nbsp();
+      buf.closeSpan();
+      src = src.substring(cr + 1);
+    }
+  }
+
   private int compare(int index, Edit edit) {
     if (index < side.getBegin(edit)) {
       return -1; // index occurs before the edit.