/**
* 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 com.google.wave.splash.text;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.waveprotocol.wave.client.editor.content.paragraph.DefaultParagraphHtmlRenderer;
import org.waveprotocol.wave.client.editor.content.paragraph.Paragraph;
import org.waveprotocol.wave.client.editor.content.paragraph.Paragraph.Alignment;
import com.google.gwt.dom.client.Style.FontWeight;
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.Line;
import com.google.wave.splash.web.template.WaveRenderer;
// TODO: Auto-generated Javadoc
/**
* 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.
*
* @author vjrj@ourproject.org (Vicente J. Ruiz Jurado)
*/
static class Marker implements Comparable<Marker> {
/**
* From annotation.
*
* @param annotation the annotation
* @param index the index
* @param isEnd the is end
* @return the marker
*/
static Marker fromAnnotation(final Annotation annotation, final int index, final boolean isEnd) {
return new Marker(annotation, index, isEnd);
}
/**
* From element.
*
* @param element the element
* @param index the index
* @return the marker
*/
static Marker fromElement(final Element element, final int index) {
return new Marker(element, index);
}
/** The annotation. */
private final Annotation annotation;
/** The element. */
private final Element element;
/** The index. */
private final int index;
/** The is end. */
private boolean isEnd;
/**
* Instantiates a new marker.
*
* @param annotation the annotation
* @param index the index
* @param isEnd the is end
*/
private Marker(final Annotation annotation, final int index, final boolean isEnd) {
this.annotation = annotation;
this.element = null;
this.index = index;
this.isEnd = isEnd;
}
/**
* Instantiates a new marker.
*
* @param element the element
* @param index the index
*/
private Marker(final Element element, final int index) {
this.element = element;
this.annotation = null;
this.index = index;
}
/* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(final Marker that) {
final int value = Integer.signum(this.index - that.index);
if (value == 0) {
if (this.isElement() != that.isElement()) {
// At boundaries, annotations should wrap elements.
final Marker annotation = this.isAnnotation() ? this : that;
return annotation.isEnd ? -1 : 1;
}
}
return value;
}
/**
* Checks if is annotation.
*
* @return true, if is annotation
*/
boolean isAnnotation() {
return this.annotation != null;
}
/**
* Checks if is element.
*
* @return true, if is element
*/
boolean isElement() {
return this.element != null;
}
}
/** The Constant LOG. */
public static final Log LOG = LogFactory.getLog(ContentRenderer.class);
// private final GadgetRenderer gadgetRenderer;
/** The identing. */
private boolean identing;
/** The in align. */
private boolean inAlign = false;
/** The inheader. */
private boolean inheader = false;
/** The wave renderer. */
private final WaveRenderer waveRenderer;
/**
* Instantiates a new content renderer.
*
* @param gadgetRenderer the gadget renderer
* @param waveRenderer the wave renderer
*/
@Inject
public ContentRenderer(final GadgetRenderer gadgetRenderer, final WaveRenderer waveRenderer) {
// this.gadgetRenderer = gadgetRenderer;
this.waveRenderer = waveRenderer;
}
/**
* Close indent if necessary.
*
* @param builder the builder
*/
private void closeIndentIfNecessary(final StringBuilder builder) {
if (identing) {
// Close identations
identing = false;
builder.append("</li>");
}
}
/**
* Emit annotation.
*
* @param marker the marker
* @param builder the builder
*/
private void emitAnnotation(final Marker marker, final StringBuilder builder) {
if (marker.annotation.getName().startsWith("link")) {
if (marker.isEnd) {
builder.append("</a>");
} else {
builder.append("<a href=\"" + marker.annotation.getValue() + "\" target=\"_blank\">");
}
} else if (marker.isEnd) {
// if (marker.annotation.getName().)
builder.append("</span>");
} else {
// Transform name into dash-separated css property rather than lower camel
// case.
String name = Markup.sanitize(marker.annotation.getName());
final String value = Markup.sanitize(marker.annotation.getValue());
// Title annotations are translated as bold.
name = name.substring(marker.annotation.getName().indexOf("/") + 1);
name = Markup.toDashedStyle(name);
builder.append("<span style='");
builder.append(name);
builder.append(':');
builder.append(value);
builder.append("'>");
}
}
/**
* Render element.
*
* @param element the element
* @param index the index
* @param contributors the contributors
* @param builder the builder
*/
private void renderElement(final Element element, final int index, final List<String> contributors,
final StringBuilder builder) {
final ElementType type = element.getType();
switch (type) {
case LINE:
final String t = element.getProperty(Line.LINE_TYPE);
final String i = element.getProperty(Line.INDENT);
final String a = element.getProperty(Line.ALIGNMENT);
// final String d = element.getProperty(Line.DIRECTION);
// For direction stuff (RTL etc) see DefaultParagraphHtml
if (inAlign) {
// Close identations
inAlign = false;
builder.append("</div><!-- end of align -->");
}
final Integer ident = i != null ? Integer.valueOf(i) : 0;
if (inheader) {
// New line, we close previous header
builder.append("</div> <!-- end h1/h2... header -->");
inheader = false;
}
// NOTE: if there exists problems with <br/> or newlines check the
// "TODO expensive and silly" comment below
if (t != null && t.equals(Paragraph.LIST_TYPE)) {
// type-0 to 2, margin 22px * i <li class="bullet-type-0"
// style="margin-left: 88px;">
// See DefaultParagraphHtml
builder.append("<li class=\"bullet-type-").append(ident % 3).append("\" style=\"margin-left: ").append(
(ident + 1) * 22).append("px;\">");
identing = true;
} else if (ident > 0) {
builder.append("<li style=\"margin-left: ").append(ident * 22).append("px;\">");
identing = true;
} else {
closeIndentIfNecessary(builder);
}
if (a != null) {
builder.append("<div style=\"text-align:" + Alignment.fromValue(a).cssValue() + ";\">");
inAlign = true;
}
if (t != null && t.startsWith("h")) {
// See DefaultParagraphHtml
final String fontWeight = FontWeight.BOLD.getCssName();
final double headingNum = Integer.parseInt(t.substring(1));
// Do this with CSS instead.
// h1 -> 1.75, h4 -> 1, others linearly in between.
final double factor = 1 - (headingNum - 1) / (Paragraph.NUM_HEADING_SIZES - 1);
final double fontSize = DefaultParagraphHtmlRenderer.MIN_HEADING_SIZE_EM
+ factor
* (DefaultParagraphHtmlRenderer.MAX_HEADING_SIZE_EM - DefaultParagraphHtmlRenderer.MIN_HEADING_SIZE_EM);
builder.append("<div style=\"font-size: ").append(fontSize).append("em; font-weight: ").append(
fontWeight).append(";\">");
inheader = true;
}
// 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("</p><p>");
} else {
// We build "<li>" with a main wrapper ul
// FIXME, close finally the ul
builder.append("<ul style=\"padding: 0px; margin: 0px;\">");
}
break;
case ATTACHMENT:
final Attachment attachment = (Attachment) element;
final 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:
// kune patch: disabled because breaks the inline replies
builder.append("<div class=\"k-gadget-signin\">Sign-in and participate to see this content.</div>");
// gadgetRenderer.render((Gadget) element, contributors, builder);
break;
case INLINE_BLIP:
waveRenderer.renderInlineReply(element, index, builder);
break;
default:
// Ignore all others.
}
}
/**
* Takes content and applies style and formatting to it based on its
* annotations and elements.
*
* @param content the content
* @param annotations the annotations
* @param elements the elements
* @param contributors the contributors
* @return the string
*/
public String renderHtml(final String content, final Annotations annotations,
final SortedMap<Integer, Element> elements, final List<String> contributors) {
final StringBuilder builder = new StringBuilder();
// NOTE(dhanji): This step is enormously important!
final char[] raw = content.toCharArray();
final SortedSet<Marker> markers = new TreeSet<Marker>();
// First add annotations sorted by range.
for (final Annotation annotation : annotations.asList()) {
// Ignore anything but style or title annotations.
final 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 (annotationName.startsWith("link")) {
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.
final int start = annotation.getRange().getStart();
final int from = raw[0] == '\n' ? 1 : 0;
final int end = content.indexOf('\n', from);
if (end > start && start < end) {
// Commented (vjrj)
// final 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 {
// LOG?
}
}
}
// Now add elements sorted by index.
for (final Map.Entry<Integer, Element> entry : elements.entrySet()) {
markers.add(Marker.fromElement(entry.getValue(), entry.getKey()));
}
builder.append("<p>");
int cursor = 0;
for (final Marker marker : markers) {
if (marker.index > cursor) {
final 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 {
emitAnnotation(marker, 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.append("</ul>").toString().replaceAll("<p>\n</p>", "<p><br/></p>").replaceAll(
"<p>\n<div style=\"font-size", "<p><br/><div style=\"font-size");
}
}