com.google.wave.splash.text.ContentRenderer.java Source code

Java tutorial

Introduction

Here is the source code for com.google.wave.splash.text.ContentRenderer.java

Source

/**
 * 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");
    }
}