// 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.common; import com.google.gerrit.reviewdb.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); if (diffPrefs.isSyntaxHighlighting() && getFileType() != null && src.isWholeFile()) { // The prettify parsers don't like ' as an entity for the // single quote character. Replace them all out so we don't // confuse the parser. // html = html.replaceAll("'", "'"); html = prettify(html, getFileType()); } else { html = expandTabs(html); html = html.replaceAll("\n", "<br />"); } 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); if (tagStart < 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; } // 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 (isBR(html, tagStart, tagEnd)) { lastTag.close(buf, html); content.addLine(src.mapIndexToLine(lineIdx), buf.toString()); buf = new StringBuilder(); col = 0; lineIdx++; } else 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 boolean isBR(String html, int tagStart, int tagEnd) { return tagEnd - tagStart == 5 // && html.charAt(tagStart + 1) == 'b' // && html.charAt(tagStart + 2) == 'r' // && html.charAt(tagStart + 3) == ' '; } 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() + "\"" // + "> </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.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)); 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. } 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 String expandTabs(String html) { StringBuilder tmp = new StringBuilder(); int i = 0; if (diffPrefs.isShowTabs()) { i = 1; } for (; i < diffPrefs.getTabSize(); i++) { tmp.append(" "); } 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; } }