/** * Copyright 2010 Google Inc. * * 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 org.waveprotocol.box.server.rpc.render.web.text; import com.google.inject.Inject; import com.google.wave.api.Annotation; import com.google.wave.api.Annotations; import com.google.wave.api.Attachment; import com.google.wave.api.Element; import com.google.wave.api.ElementType; import com.google.wave.api.Gadget; import org.waveprotocol.box.server.rpc.render.web.template.Templates; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeSet; /** * A utility class that converts blip content into html. * * @author David Byttow * @author dhanji@gmail.com (Dhanji R. Prasanna) */ public class ContentRenderer { /** * Represents a marker in content that is either the start or end of * an annotation or a single element. */ static class Marker implements Comparable<Marker> { static Marker fromAnnotation(Annotation annotation, int index, boolean isEnd) { return new Marker(annotation, index, isEnd); } static Marker fromElement(Element element, int index) { return new Marker(element, index); } private final int index; private final Annotation annotation; private final Element element; boolean isEnd; private Marker(Annotation annotation, int index, boolean isEnd) { this.annotation = annotation; this.element = null; this.index = index; this.isEnd = isEnd; } private Marker(Element element, int index) { this.element = element; this.annotation = null; this.index = index; } boolean isElement() { return this.element != null; } boolean isAnnotation() { return this.annotation != null; } @Override public int compareTo(Marker that) { int value = Integer.signum(this.index - that.index); if (value == 0) { if (this.isElement() != that.isElement()) { // At boundaries, annotations should wrap elements. Marker annotation = this.isAnnotation() ? this : that; return annotation.isEnd ? -1 : 1; } } return value; } public void emit(StringBuilder builder) { if (annotation.getName().startsWith("link")) { emitHrefAnnotation(builder); } else if (annotation.getName().startsWith("style")) { emitStyleAnnotation(builder); } } private void emitStyleAnnotation(StringBuilder builder) { if (isEnd) { builder.append("</span>"); } else { // Transform name into dash-separated css property rather than lower camel case. String name = Markup.sanitize(annotation.getName()); String value = Markup.sanitize(annotation.getValue()); // Title annotations are translated as bold. name = name.substring(annotation.getName().indexOf("/") + 1); name = Markup.toDashedStyle(name); builder.append("<span style='"); builder.append(name); builder.append(':'); builder.append(value); builder.append("'>"); } } private void emitHrefAnnotation(StringBuilder builder) { if (isEnd) { builder.append("</a>"); } else { String value = Markup.sanitize(annotation.getValue()); builder.append("<a href='"); builder.append(value); builder.append("' target='_blank'>"); } } } private final GadgetRenderer gadgetRenderer; @Inject public ContentRenderer(GadgetRenderer gadgetRenderer) { this.gadgetRenderer = gadgetRenderer; } public ContentRenderer() { this.gadgetRenderer = new GadgetRenderer() { @Override public void render(Gadget gadget, List<String> contributors, StringBuilder builder) { builder.append("<div>Gadget: " + gadget.getUrl() + "</div>"); } }; } /** * Takes content and applies style and formatting to it based on its * annotations and elements. */ public String renderHtml(String content, Annotations annotations, SortedMap<Integer, Element> elements, List<String> contributors) { StringBuilder builder = new StringBuilder(); // NOTE(dhanji): This step is enormously important! char[] raw = content.toCharArray(); SortedSet<Marker> markers = new TreeSet<Marker>(); // First add annotations sorted by range. for (Annotation annotation : annotations.asList()) { // Ignore anything but style or title annotations. String annotationName = annotation.getName(); if (annotationName.startsWith("style")) { markers.add(Marker.fromAnnotation(annotation, annotation.getRange().getStart(), false)); markers.add(Marker.fromAnnotation(annotation, annotation.getRange().getEnd(), true)); } else if ("conv/title".equals(annotationName)) { // Find the first newline and make sure the annotation only gets to that // point. int start = annotation.getRange().getStart(); int from = raw[0] == '\n' ? 1 : 0; int end = content.indexOf('\n', from); if (end > start && start < end) { Annotation title = new Annotation(Annotation.FONT_WEIGHT, "bold", start, end); markers.add(Marker.fromAnnotation(title, start, false)); markers.add(Marker.fromAnnotation(title, end, true)); } } else if(annotationName.startsWith("link")) { markers.add(Marker.fromAnnotation(annotation, annotation.getRange().getStart(), false)); markers.add(Marker.fromAnnotation(annotation, annotation.getRange().getEnd(), true)); } } // Now add elements sorted by index. for (Map.Entry<Integer, Element> entry : elements.entrySet()) { markers.add(Marker.fromElement(entry.getValue(), entry.getKey())); } int cursor = 0; for (Marker marker : markers) { if (marker.index > cursor) { int to = Math.min(raw.length, marker.index); builder.append(Markup.sanitize(new String(raw, cursor, to - cursor))); } cursor = marker.index; if (marker.isElement()) { renderElement(marker.element, marker.index, contributors, builder); } else { marker.emit(builder); } } // add any tail bits if (cursor < raw.length - 1) { builder.append(Markup.sanitize(new String(raw, cursor, raw.length - cursor))); } // Replace empty paragraphs. (TODO expensive and silly) return builder.toString().replace("<p>\n</p>", "<p><br/></p>"); } private void renderElement(Element element, int index, List<String> contributors, StringBuilder builder) { ElementType type = element.getType(); switch (type) { case LINE: // TODO(anthonybaxter): need to handle <line t="li"> and <line t="li" i="3"> // TODO(anthonybaxter): also handle H1 &c // Special case: If this is the first LINE element at position 0, // ignore it because we've already appended the first <p> tag. if (index > 0) { builder.append("<br/>"); } break; case ATTACHMENT: Attachment attachment = (Attachment) element; String url = Markup.sanitizeAndEncode(attachment.getAttachmentUrl()); String caption = Markup.sanitize(element.getProperty("caption")); if (caption == null) { caption = ""; } // TODO: Revisit this questionable html. builder.append("<table class=\"attachment-element\"><tr><td>") .append("<a class=\"lightbox\" title=\"") .append(caption) .append("\" href=\"") .append(url) .append("\"><img src=\"") .append(url) .append("\"/></a></td></tr></td></tr><tr><td><div class=\"caption\">") .append(caption) .append("</div></td></tr></table>"); break; case IMAGE: String imageUrl = element.getProperty("url"); if (Markup.isTrustedImageUrl(imageUrl)) { imageUrl = Markup.sanitizeAndEncode(imageUrl); builder.append("<img src=\"").append(imageUrl).append("\"/>"); } break; case GADGET: gadgetRenderer.render((Gadget) element, contributors, builder); break; case INLINE_BLIP: String id = element.getProperty("id"); builder.append(Templates.makeAnchorTag(id) ); break; default: // Ignore all others. } } }