| // Copyright (C) 2009 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.gwtexpui.safehtml.client; |
| |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.regexp.shared.MatchResult; |
| import com.google.gwt.regexp.shared.RegExp; |
| import com.google.gwt.user.client.DOM; |
| import com.google.gwt.user.client.ui.HTML; |
| import com.google.gwt.user.client.ui.HTMLTable; |
| import com.google.gwt.user.client.ui.HasHTML; |
| import com.google.gwt.user.client.ui.InlineHTML; |
| import com.google.gwt.user.client.ui.Widget; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| /** Immutable string safely placed as HTML without further escaping. */ |
| @SuppressWarnings("serial") |
| public abstract class SafeHtml implements com.google.gwt.safehtml.shared.SafeHtml { |
| public static final SafeHtmlResources RESOURCES; |
| |
| static { |
| if (GWT.isClient()) { |
| RESOURCES = GWT.create(SafeHtmlResources.class); |
| RESOURCES.css().ensureInjected(); |
| |
| } else { |
| RESOURCES = |
| new SafeHtmlResources() { |
| @Override |
| public SafeHtmlCss css() { |
| return new SafeHtmlCss() { |
| @Override |
| public String wikiList() { |
| return "wikiList"; |
| } |
| |
| @Override |
| public String wikiPreFormat() { |
| return "wikiPreFormat"; |
| } |
| |
| @Override |
| public String wikiQuote() { |
| return "wikiQuote"; |
| } |
| |
| @Override |
| public boolean ensureInjected() { |
| return false; |
| } |
| |
| @Override |
| public String getName() { |
| return null; |
| } |
| |
| @Override |
| public String getText() { |
| return null; |
| } |
| }; |
| } |
| }; |
| } |
| } |
| |
| /** @return the existing HTML property of a widget. */ |
| public static SafeHtml get(HasHTML t) { |
| return new SafeHtmlString(t.getHTML()); |
| } |
| |
| /** @return the existing HTML text, wrapped in a safe buffer. */ |
| public static SafeHtml asis(String htmlText) { |
| return new SafeHtmlString(htmlText); |
| } |
| |
| /** Set the HTML property of a widget. */ |
| public static <T extends HasHTML> T set(T e, SafeHtml str) { |
| e.setHTML(str.asString()); |
| return e; |
| } |
| |
| /** @return the existing inner HTML of any element. */ |
| public static SafeHtml get(Element e) { |
| return new SafeHtmlString(e.getInnerHTML()); |
| } |
| |
| /** Set the inner HTML of any element. */ |
| public static Element setInnerHTML(Element e, SafeHtml str) { |
| e.setInnerHTML(str.asString()); |
| return e; |
| } |
| |
| /** @return the existing inner HTML of a table cell. */ |
| public static SafeHtml get(HTMLTable t, int row, int col) { |
| return new SafeHtmlString(t.getHTML(row, col)); |
| } |
| |
| /** Set the inner HTML of a table cell. */ |
| public static <T extends HTMLTable> T set(final T t, int row, int col, SafeHtml str) { |
| t.setHTML(row, col, str.asString()); |
| return t; |
| } |
| |
| /** Parse an HTML block and return the first (typically root) element. */ |
| public static Element parse(SafeHtml html) { |
| Element e = DOM.createDiv(); |
| setInnerHTML(e, html); |
| return DOM.getFirstChild(e); |
| } |
| |
| /** Convert bare http:// and https:// URLs into <a href> tags. */ |
| public SafeHtml linkify() { |
| final String part = "(?:[a-zA-Z0-9$_+!*'%;:@=?#/~-]|&(?!lt;|gt;)|[.,](?!(?:\\s|$)))"; |
| return replaceAll( |
| "(https?://" + part + "{2,}(?:[(]" + part + "*[)])*" + part + "*)", |
| "<a href=\"$1\" target=\"_blank\" rel=\"nofollow\">$1</a>"); |
| } |
| |
| /** |
| * Apply {@link #linkify()}, and "\n\n" to <p>. |
| * |
| * <p>Lines that start with whitespace are assumed to be preformatted, and are formatted by the |
| * {@link SafeHtmlCss#wikiPreFormat()} CSS class. |
| */ |
| public SafeHtml wikify() { |
| final SafeHtmlBuilder r = new SafeHtmlBuilder(); |
| for (String p : linkify().asString().split("\n\n")) { |
| if (isQuote(p)) { |
| wikifyQuote(r, p); |
| |
| } else if (isPreFormat(p)) { |
| r.openElement("p"); |
| for (String line : p.split("\n")) { |
| r.openSpan(); |
| r.setStyleName(RESOURCES.css().wikiPreFormat()); |
| r.append(asis(line)); |
| r.closeSpan(); |
| r.br(); |
| } |
| r.closeElement("p"); |
| |
| } else if (isList(p)) { |
| wikifyList(r, p); |
| |
| } else { |
| r.openElement("p"); |
| r.append(asis(p)); |
| r.closeElement("p"); |
| } |
| } |
| return r.toSafeHtml(); |
| } |
| |
| private void wikifyList(SafeHtmlBuilder r, String p) { |
| boolean in_ul = false; |
| boolean in_p = false; |
| for (String line : p.split("\n")) { |
| if (line.startsWith("-") || line.startsWith("*")) { |
| if (!in_ul) { |
| if (in_p) { |
| in_p = false; |
| r.closeElement("p"); |
| } |
| |
| in_ul = true; |
| r.openElement("ul"); |
| r.setStyleName(RESOURCES.css().wikiList()); |
| } |
| line = line.substring(1).trim(); |
| |
| } else if (!in_ul) { |
| if (!in_p) { |
| in_p = true; |
| r.openElement("p"); |
| } else { |
| r.append(' '); |
| } |
| r.append(asis(line)); |
| continue; |
| } |
| |
| r.openElement("li"); |
| r.append(asis(line)); |
| r.closeElement("li"); |
| } |
| |
| if (in_ul) { |
| r.closeElement("ul"); |
| } else if (in_p) { |
| r.closeElement("p"); |
| } |
| } |
| |
| private void wikifyQuote(SafeHtmlBuilder r, String p) { |
| r.openElement("blockquote"); |
| r.setStyleName(RESOURCES.css().wikiQuote()); |
| if (p.startsWith("> ")) { |
| p = p.substring(5); |
| } else if (p.startsWith(" > ")) { |
| p = p.substring(6); |
| } |
| p = p.replaceAll("\\n ?> ", "\n"); |
| for (String e : p.split("\n\n")) { |
| if (isQuote(e)) { |
| SafeHtmlBuilder b = new SafeHtmlBuilder(); |
| wikifyQuote(b, e); |
| r.append(b); |
| } else { |
| r.append(asis(e)); |
| } |
| } |
| r.closeElement("blockquote"); |
| } |
| |
| private static boolean isQuote(String p) { |
| return p.startsWith("> ") || p.startsWith(" > "); |
| } |
| |
| private static boolean isPreFormat(String p) { |
| return p.contains("\n ") || p.contains("\n\t") || p.startsWith(" ") || p.startsWith("\t"); |
| } |
| |
| private static boolean isList(String p) { |
| return p.contains("\n- ") || p.contains("\n* ") || p.startsWith("- ") || p.startsWith("* "); |
| } |
| |
| /** |
| * Replace first occurrence of {@code regex} with {@code repl} . |
| * |
| * <p><b>WARNING:</b> This replacement is being performed against an otherwise safe HTML string. |
| * The caller must ensure that the replacement does not introduce cross-site scripting attack |
| * entry points. |
| * |
| * @param regex regular expression pattern to match the substring with. |
| * @param repl replacement expression. Capture groups within {@code regex} can be referenced with |
| * {@code $<i>n</i>}. |
| * @return a new string, after the replacement has been made. |
| */ |
| public SafeHtml replaceFirst(String regex, String repl) { |
| return new SafeHtmlString(asString().replaceFirst(regex, repl)); |
| } |
| |
| /** |
| * Replace each occurrence of {@code regex} with {@code repl} . |
| * |
| * <p><b>WARNING:</b> This replacement is being performed against an otherwise safe HTML string. |
| * The caller must ensure that the replacement does not introduce cross-site scripting attack |
| * entry points. |
| * |
| * @param regex regular expression pattern to match substrings with. |
| * @param repl replacement expression. Capture groups within {@code regex} can be referenced with |
| * {@code $<i>n</i>}. |
| * @return a new string, after the replacements have been made. |
| */ |
| public SafeHtml replaceAll(String regex, String repl) { |
| return new SafeHtmlString(asString().replaceAll(regex, repl)); |
| } |
| |
| /** |
| * Replace all find/replace pairs in the list in a single pass. |
| * |
| * @param findReplaceList find/replace pairs to use. |
| * @return a new string, after the replacements have been made. |
| */ |
| public <T> SafeHtml replaceAll(List<? extends FindReplace> findReplaceList) { |
| if (findReplaceList == null || findReplaceList.isEmpty()) { |
| return this; |
| } |
| |
| StringBuilder pat = new StringBuilder(); |
| Iterator<? extends FindReplace> it = findReplaceList.iterator(); |
| while (it.hasNext()) { |
| FindReplace fr = it.next(); |
| pat.append(fr.pattern().getSource()); |
| if (it.hasNext()) { |
| pat.append('|'); |
| } |
| } |
| |
| StringBuilder result = new StringBuilder(); |
| RegExp re = RegExp.compile(pat.toString(), "g"); |
| String orig = asString(); |
| int index = 0; |
| MatchResult mat; |
| while ((mat = re.exec(orig)) != null) { |
| String g = mat.getGroup(0); |
| // Re-run each candidate to find which one matched. |
| for (FindReplace fr : findReplaceList) { |
| if (fr.pattern().test(g)) { |
| try { |
| String repl = fr.replace(g); |
| result.append(orig.substring(index, mat.getIndex())); |
| result.append(repl); |
| } catch (IllegalArgumentException e) { |
| continue; |
| } |
| index = mat.getIndex() + g.length(); |
| break; |
| } |
| } |
| } |
| result.append(orig.substring(index, orig.length())); |
| return asis(result.toString()); |
| } |
| |
| /** @return a GWT block display widget displaying this HTML. */ |
| public Widget toBlockWidget() { |
| return new HTML(asString()); |
| } |
| |
| /** @return a GWT inline display widget displaying this HTML. */ |
| public Widget toInlineWidget() { |
| return new InlineHTML(asString()); |
| } |
| |
| /** @return a clean HTML string safe for inclusion in any context. */ |
| @Override |
| public abstract String asString(); |
| } |