summaryrefslogtreecommitdiffstats
path: root/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
diff options
context:
space:
mode:
Diffstat (limited to 'gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java')
-rw-r--r--gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java559
1 files changed, 559 insertions, 0 deletions
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
new file mode 100644
index 0000000000..a84af5ee02
--- /dev/null
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/PrettyFormatter.java
@@ -0,0 +1,559 @@
+// Copyright (C) 2010 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.google.gerrit.prettify.client;
+
+import com.google.gerrit.prettify.common.SparseFileContent;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.diff.ReplaceEdit;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public abstract class PrettyFormatter implements SparseHtmlFile {
+ public static abstract class EditFilter {
+ abstract String getStyleName();
+
+ abstract int getBegin(Edit edit);
+
+ abstract int getEnd(Edit edit);
+ }
+
+ public static final EditFilter A = new EditFilter() {
+ @Override
+ String getStyleName() {
+ return "wdd";
+ }
+
+ @Override
+ int getBegin(Edit edit) {
+ return edit.getBeginA();
+ }
+
+ @Override
+ int getEnd(Edit edit) {
+ return edit.getEndA();
+ }
+ };
+
+ public static final EditFilter B = new EditFilter() {
+ @Override
+ String getStyleName() {
+ return "wdi";
+ }
+
+ @Override
+ int getBegin(Edit edit) {
+ return edit.getBeginB();
+ }
+
+ @Override
+ int getEnd(Edit edit) {
+ return edit.getEndB();
+ }
+ };
+
+ protected SparseFileContent content;
+ protected EditFilter side;
+ protected List<Edit> edits;
+ protected AccountDiffPreference diffPrefs;
+ protected String fileName;
+ protected Set<Integer> trailingEdits;
+
+ private int col;
+ private int lineIdx;
+ private Tag lastTag;
+ private StringBuilder buf;
+
+ public SafeHtml getSafeHtmlLine(int lineNo) {
+ return SafeHtml.asis(content.get(lineNo));
+ }
+
+ public int size() {
+ return content.size();
+ }
+
+ @Override
+ public boolean contains(int idx) {
+ return content.contains(idx);
+ }
+
+ @Override
+ public boolean hasTrailingEdit(int idx) {
+ return trailingEdits.contains(idx);
+ }
+
+ public void setEditFilter(EditFilter f) {
+ side = f;
+ }
+
+ public void setEditList(List<Edit> all) {
+ edits = all;
+ }
+
+ public void setDiffPrefs(AccountDiffPreference how) {
+ diffPrefs = how;
+ }
+
+ public void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ /**
+ * Parse and format a complete source code file.
+ *
+ * @param src raw content of the file to format. The line strings will be HTML
+ * escaped before processing, so it must be the raw text.
+ */
+ public void format(SparseFileContent src) {
+ content = new SparseFileContent();
+ content.setSize(src.size());
+ trailingEdits = new HashSet<Integer>();
+
+ String html = toHTML(src);
+
+ html = expandTabs(html);
+ if (diffPrefs.isSyntaxHighlighting() && getFileType() != null
+ && src.isWholeFile()) {
+ // The prettify parsers don't like &#39; as an entity for the
+ // single quote character. Replace them all out so we don't
+ // confuse the parser.
+ //
+ html = html.replaceAll("&#39;", "'");
+
+ // If a line is modified at its end and the line ending is changed from
+ // '\n' to '\r\n' then the '\r' of the new line is part of the modified
+ // text. If intraline diffs are highlighted the modified text is
+ // surrounded by a 'span' tag. As result '\r' and '\n' of the new line get
+ // separated by '</span>'. For the prettify parser this now looks like two
+ // separate line endings. This messes up the line counting below.
+ // Drop any '\r' to avoid this problem.
+ html = html.replace("\r</span>\n", "</span>\n");
+
+ html = html.replaceAll("(\r)?\n", " $1\n");
+ html = prettify(html, getFileType());
+ html = html.replaceAll(" (\r)?\n", "$1\n");
+ }
+
+ int pos = 0;
+ int textChunkStart = 0;
+
+ lastTag = Tag.NULL;
+ col = 0;
+ lineIdx = 0;
+
+ buf = new StringBuilder();
+ while (pos <= html.length()) {
+ int tagStart = html.indexOf('<', pos);
+ int lf = html.indexOf('\n', pos);
+
+ if (tagStart < 0 && lf < 0) {
+ // No more tags remaining. What's left is plain text.
+ //
+ assert lastTag == Tag.NULL;
+ pos = html.length();
+ if (textChunkStart < pos) {
+ htmlText(html.substring(textChunkStart, pos));
+ }
+ if (0 < buf.length()) {
+ content.addLine(src.mapIndexToLine(lineIdx), buf.toString());
+ }
+ break;
+ }
+
+ // Line end occurs before the next HTML tag. Break the line.
+ if (0 <= lf && (lf < tagStart || tagStart < 0)) {
+ if (textChunkStart < lf) {
+ lastTag.open(buf, html);
+ htmlText(html.substring(textChunkStart, lf));
+ }
+ pos = lf + 1;
+ textChunkStart = pos;
+
+ lastTag.close(buf, html);
+ content.addLine(src.mapIndexToLine(lineIdx++), buf.toString());
+ buf = new StringBuilder();
+ col = 0;
+ continue;
+ }
+
+ // Assume no attribute contains '>' and that all tags
+ // within the HTML will be well-formed.
+ //
+ int tagEnd = html.indexOf('>', tagStart);
+ assert tagStart < tagEnd;
+ pos = tagEnd + 1;
+
+ // Handle any text between the end of the last tag,
+ // and the start of this tag.
+ //
+ if (textChunkStart < tagStart) {
+ lastTag.open(buf, html);
+ htmlText(html.substring(textChunkStart, tagStart));
+ }
+ textChunkStart = pos;
+
+ if (html.charAt(tagStart + 1) == '/') {
+ lastTag = lastTag.pop(buf, html);
+
+ } else if (html.charAt(tagEnd - 1) != '/') {
+ lastTag = new Tag(lastTag, tagStart, tagEnd);
+ }
+ }
+ buf = null;
+ }
+
+ private void htmlText(String txt) {
+ int pos = 0;
+ while (pos < txt.length()) {
+ int start = txt.indexOf('&', pos);
+ if (start < 0) {
+ break;
+ }
+
+ cleanText(txt, pos, start);
+ pos = txt.indexOf(';', start + 1) + 1;
+
+ if (diffPrefs.getLineLength() <= col) {
+ buf.append("<br />");
+ col = 0;
+ }
+
+ buf.append(txt.substring(start, pos));
+ col++;
+ }
+
+ cleanText(txt, pos, txt.length());
+ }
+
+ private void cleanText(String txt, int pos, int end) {
+ while (pos < end) {
+ int free = diffPrefs.getLineLength() - col;
+ if (free <= 0) {
+ // The current line is full. Throw an explicit line break
+ // onto the end, and we'll continue on the next line.
+ //
+ buf.append("<br />");
+ col = 0;
+ free = diffPrefs.getLineLength();
+ }
+
+ int n = Math.min(end - pos, free);
+ buf.append(txt.substring(pos, pos + n));
+ col += n;
+ pos += n;
+ }
+ }
+
+ /** Run the prettify engine over the text and return the result. */
+ protected abstract String prettify(String html, String type);
+
+ private static class Tag {
+ static final Tag NULL = new Tag(null, 0, 0) {
+ @Override
+ void open(StringBuilder buf, String html) {
+ }
+
+ @Override
+ void close(StringBuilder buf, String html) {
+ }
+
+ @Override
+ Tag pop(StringBuilder buf, String html) {
+ return this;
+ }
+ };
+
+ final Tag parent;
+ final int start;
+ final int end;
+ boolean open;
+
+ Tag(Tag p, int s, int e) {
+ parent = p;
+ start = s;
+ end = e;
+ }
+
+ void open(StringBuilder buf, String html) {
+ if (!open) {
+ parent.open(buf, html);
+ buf.append(html.substring(start, end + 1));
+ open = true;
+ }
+ }
+
+ void close(StringBuilder buf, String html) {
+ pop(buf, html);
+ parent.close(buf, html);
+ }
+
+ Tag pop(StringBuilder buf, String html) {
+ if (open) {
+ int sp = html.indexOf(' ', start + 1);
+ if (sp < 0 || end < sp) {
+ sp = end;
+ }
+
+ buf.append("</");
+ buf.append(html.substring(start + 1, sp));
+ buf.append('>');
+ open = false;
+ }
+ return parent;
+ }
+ }
+
+ private String toHTML(SparseFileContent src) {
+ SafeHtml html;
+
+ if (diffPrefs.isIntralineDifference()) {
+ html = colorLineEdits(src);
+ } else {
+ SafeHtmlBuilder b = new SafeHtmlBuilder();
+ for (int index = src.first(); index < src.size(); index = src.next(index)) {
+ b.append(src.get(index));
+ 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 (diffPrefs.isShowWhitespaceErrors()) {
+ // We need to do whitespace errors before showing tabs, because
+ // these patterns rely on \t as a literal, before it expands.
+ //
+ html = showTabAfterSpace(html);
+ html = showTrailingWhitespace(html);
+ }
+
+ if (diffPrefs.isShowLineEndings()){
+ html = showLineEndings(html);
+ }
+
+ if (diffPrefs.isShowTabs()) {
+ String t = 1 < diffPrefs.getTabSize() ? "\t" : "";
+ html = html.replaceAll("\t", "<span class=\"vt\">\u00BB</span>" + t);
+ }
+
+ return html.asString();
+ }
+
+ private SafeHtml colorLineEdits(SparseFileContent src) {
+ // Make a copy of the edits with a sentinel that is after all lines
+ // in the source. That simplifies our loop below because we'll never
+ // run off the end of the edit list.
+ //
+ List<Edit> edits = new ArrayList<Edit>(this.edits.size() + 1);
+ edits.addAll(this.edits);
+ edits.add(new Edit(src.size(), src.size()));
+
+ SafeHtmlBuilder buf = new SafeHtmlBuilder();
+
+ int curIdx = 0;
+ Edit curEdit = edits.get(curIdx);
+
+ ReplaceEdit lastReplace = null;
+ List<Edit> charEdits = null;
+ int lastPos = 0;
+ int lastIdx = 0;
+
+ for (int index = src.first(); index < src.size(); index = src.next(index)) {
+ int cmp = compare(index, curEdit);
+ while (0 < cmp) {
+ // The index is after the edit. Skip to the next edit.
+ //
+ curEdit = edits.get(curIdx++);
+ cmp = compare(index, curEdit);
+ }
+
+ if (cmp < 0) {
+ // index occurs before the edit. This is a line of context.
+ //
+ appendShowBareCR(buf, src.get(index), true);
+ buf.append('\n');
+ continue;
+ }
+
+ // index occurs within the edit. The line is a modification.
+ //
+ if (curEdit instanceof ReplaceEdit) {
+ if (lastReplace != curEdit) {
+ lastReplace = (ReplaceEdit) curEdit;
+ charEdits = lastReplace.getInternalEdits();
+ lastPos = 0;
+ lastIdx = 0;
+ }
+
+ String line = src.get(index) + "\n";
+ for (int c = 0; c < line.length();) {
+ if (charEdits.size() <= lastIdx) {
+ appendShowBareCR(buf, line.substring(c), false);
+ break;
+ }
+
+ final Edit edit = charEdits.get(lastIdx);
+ final int b = side.getBegin(edit) - lastPos;
+ final int e = side.getEnd(edit) - lastPos;
+
+ if (c < b) {
+ // There is text at the start of this line that is common
+ // with the other side. Copy it with no style around it.
+ //
+ final int cmnLen = Math.min(b, line.length());
+ buf.openSpan();
+ buf.setStyleName("wdc");
+ appendShowBareCR(buf, line.substring(c, cmnLen), //
+ cmnLen == line.length() - 1);
+ buf.closeSpan();
+ c = cmnLen;
+ }
+
+ final int modLen = Math.min(e, line.length());
+ if (c < e && c < modLen) {
+ buf.openSpan();
+ buf.setStyleName(side.getStyleName());
+ appendShowBareCR(buf, line.substring(c, modLen), //
+ modLen == line.length() - 1);
+ buf.closeSpan();
+ if (modLen == line.length()) {
+ trailingEdits.add(index);
+ }
+ c = modLen;
+ }
+
+ if (e <= c) {
+ lastIdx++;
+ }
+ }
+ lastPos += line.length();
+
+ } else {
+ 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 + 1));
+ return;
+ }
+ } else if (cr == src.length() - 2 && src.charAt(cr + 1) == '\n') {
+ buf.append(src);
+ 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.
+
+ } else if (index < side.getEnd(edit)) {
+ return 0; // index occurs within the edit.
+
+ } else {
+ return 1; // index occurs after the edit.
+ }
+ }
+
+ private SafeHtml showTabAfterSpace(SafeHtml src) {
+ final String m = "( ( |<span[^>]*>|</span>)*\t)";
+ final String r = "<span class=\"wse\"" //
+ + " title=\"" + PrettifyConstants.C.wseTabAfterSpace() + "\"" //
+ + ">$1</span>";
+ src = src.replaceFirst("^" + m, r);
+ src = src.replaceAll("\n" + m, "\n" + r);
+ return src;
+ }
+
+ private SafeHtml showTrailingWhitespace(SafeHtml src) {
+ final String r = "<span class=\"wse\"" //
+ + " title=\"" + PrettifyConstants.C.wseTrailingSpace() + "\"" //
+ + ">$1</span>$2";
+ src = src.replaceAll("([ \t][ \t]*)(\r?(</span>)?\n)", r);
+ src = src.replaceFirst("([ \t][ \t]*)(\r?(</span>)?\n?)$", r);
+ return src;
+ }
+
+ private SafeHtml showLineEndings(SafeHtml src) {
+ final String r = "<span class=\"lecr\""
+ + " title=\"" + PrettifyConstants.C.leCR() + "\"" //
+ + ">\\\\r</span>";
+ src = src.replaceAll("\r", r);
+ return src;
+ }
+
+ private String expandTabs(String html) {
+ StringBuilder tmp = new StringBuilder();
+ int i = 0;
+ if (diffPrefs.isShowTabs()) {
+ i = 1;
+ }
+ for (; i < diffPrefs.getTabSize(); i++) {
+ tmp.append("&nbsp;");
+ }
+ return html.replaceAll("\t", tmp.toString());
+ }
+
+ private String getFileType() {
+ String srcType = fileName;
+ if (srcType == null) {
+ return null;
+ }
+
+ int dot = srcType.lastIndexOf('.');
+ if (dot < 0) {
+ return null;
+ }
+
+ if (0 < dot) {
+ srcType = srcType.substring(dot + 1);
+ }
+
+ if ("txt".equalsIgnoreCase(srcType)) {
+ return null;
+ }
+
+ return srcType;
+ }
+}