Package org.waveprotocol.wave.model.richtext

Source Code of org.waveprotocol.wave.model.richtext.RichTextMutationBuilder

/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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.wave.model.richtext;

import org.waveprotocol.wave.model.conversation.AnnotationConstants;
import org.waveprotocol.wave.model.document.ReadableDocument;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.document.operation.Nindo;
import org.waveprotocol.wave.model.document.operation.automaton.DocumentSchema.PermittedCharacters;
import org.waveprotocol.wave.model.document.operation.impl.AttributesImpl;
import org.waveprotocol.wave.model.document.operation.util.ImmutableStateMap.Attribute;
import org.waveprotocol.wave.model.document.util.LineContainers;
import org.waveprotocol.wave.model.document.util.Point;
import org.waveprotocol.wave.model.richtext.RichTextTokenizer.Type;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.Preconditions;
import org.waveprotocol.wave.model.util.ReadableStringSet;
import org.waveprotocol.wave.model.util.StringMap;
import org.waveprotocol.wave.model.util.StringSet;
import org.waveprotocol.wave.model.util.ValueUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

/**
* Builds up a sequence of operations from DOM that has been parsed with
* a RichTextTokenizer.
*
*/
public class RichTextMutationBuilder {
  // TODO(user): Centralize these constants.
  private static final String TYPE_ATTR = "t";

  private static final String LI_STYLE_ATTR = "listyle";

  private static final String INDENT_ATTR = "i";

  /** Default annotation values for certain keys */
  private static final StringMap<ReadableStringSet> defaultAnnotations =
      CollectionUtils.createStringMap();

  static {
    defaultAnnotations.put(AnnotationConstants.STYLE_TEXT_DECORATION, CollectionUtils.newStringSet("none"));
    defaultAnnotations.put(AnnotationConstants.STYLE_FONT_WEIGHT, CollectionUtils.newStringSet("normal"));
    defaultAnnotations.put(AnnotationConstants.STYLE_FONT_STYLE, CollectionUtils.newStringSet("normal"));
    defaultAnnotations.put(AnnotationConstants.STYLE_BG_COLOR, CollectionUtils.newStringSet("initial",
        "transparent"));
    // Default font family and color are dependent on user-agent settings, but
    // make up a default color anyway
    defaultAnnotations.put(AnnotationConstants.STYLE_COLOR, CollectionUtils.newStringSet("black"));
  }

  /** This map records all annotations currently started. */
  private final Map<String, Stack<String>> startedAnnotations =
      new HashMap<String, Stack<String>>();

  /** The current delta offset. */
  private int offset;

  /** Current indentation level */
  private final List<Type> structureStack = new ArrayList<Type>();

  /** Count of open elements. */
  private int elementCount;

  /** Last valid cursor position relative from the start. */
  private int lastGoodCursorOffset;

  private int indentationLevel = 0;

  private boolean isFirstToken;

  /**
   * Annotation keys that have been touched.
   */
  private final StringSet affectedKeys = CollectionUtils.createStringSet();

  /** Mapping of key/value pairs to use when particular keys have no mappings in the builder. */
  private final StringMap<String> defaultValueMap;

  public RichTextMutationBuilder() {
    this(CollectionUtils.<String>createStringMap());
  }

  /**
   * @param defaultValueMap annotation (key, value) pairs that are to be used
   *        when no explicit annotation value is set for that key.
   */
  public RichTextMutationBuilder(StringMap<String> defaultValueMap) {
    this.defaultValueMap = defaultValueMap;
    reset();
  }


  private final void reset() {
    offset = 0;
    structureStack.clear();
    elementCount = 0;
    lastGoodCursorOffset = 0;
    startedAnnotations.clear();
    affectedKeys.clear();
    isFirstToken = true;
  }

  /**
   * Returns the offset location that is safe to place the cursor. It is
   * relative from the current location when mutations were applied. For
   * example, if <p></p> is inserted, the full offset would be 2 while this
   * method would return 1 (to be placed within the p).
   *
   * @return A good location offset.
   */
  public int getLastGoodCursorOffset() {
    return lastGoodCursorOffset;
  }

  /**
   * Applies the mutations to a document mutation builder based on a tokenized
   * stream of rich text. Assumes that the current location of the document
   * mutation builder is inside of a <p> tag. Returns a delta offset from
   * the current location typically used to place the caret at the location of
   * the last mutation.
   *
   * @param <N> Node type.
   * @param <E> Element type.
   * @param <T> Text type.
   * @param tokenizer The processed tokenizer.
   * @param builder The Nindo builder.
   * @param doc Readable document used to get element information.
   * @param splitContainer In case of an initial split, this is the container
   *        to use.
   * @return The annotation keys that have been affected.
   */
  public <N, E extends N, T extends N> ReadableStringSet applyMutations(RichTextTokenizer tokenizer,
      Nindo.Builder builder,
      ReadableDocument<N, E, T> doc,
      N splitContainer) {
    reset();

    while (tokenizer.hasNext()) {
      Type tokenType = tokenizer.next();

      switch (tokenType) {
        case NEW_LINE:
        case LIST_ITEM:
          handleNewLine(tokenizer, builder, doc, splitContainer,
              structuralAttributes(tokenizer));
          break;
        default:
          handleBasicMutation(tokenizer, builder, doc, splitContainer);
          break;
      }
      isFirstToken = false;
    }

    // Make sure all annotations have been ended.
    for (String key : startedAnnotations.keySet()) {
      builder.endAnnotation(key);
    }

    lastGoodCursorOffset = offset;

    // Close any remaining open tags. Not adding this to offset because we want
    // the cursor to remain.
    for (int i = 0; i < elementCount; ++i) {
      builder.elementEnd();
      ++offset;
    }
    return affectedKeys;
  }

  private Attributes structuralAttributes(RichTextTokenizer tokenizer) {
    Type currentStructureType = peek();
    List<Attribute> attrList = new ArrayList<Attribute>();

    if (currentStructureType != null) {
      int indent = indentationLevel;

      if (tokenizer.getCurrentType() == Type.LIST_ITEM) {
        switch (currentStructureType) {
        case UNORDERED_LIST_START:
          indent--;
          attrList.add(new Attribute(TYPE_ATTR, "li"));
          break;
        case ORDERED_LIST_START:
          indent--;
          attrList.add(new Attribute(TYPE_ATTR, "li"));
          attrList.add(new Attribute(LI_STYLE_ATTR, "decimal"));
          break;
        }
      } else if (tokenizer.getData() != null) {
        attrList.add(new Attribute(TYPE_ATTR, tokenizer.getData()));
      }

      if (indent > 0) {
        attrList.add(new Attribute(INDENT_ATTR, "" + indent));
      }
    }

    AttributesImpl attributes = AttributesImpl.fromUnsortedAttributes(attrList);

    return attributes;
  }

  private <N, E extends N, T extends N> void handleBasicMutation(RichTextTokenizer tokenizer,
      Nindo.Builder builder, ReadableDocument<N, E, T> doc, N splitContainer) {
    Type currentType = tokenizer.getCurrentType();
    switch (currentType) {
      case TEXT:
        if (tokenizer.getData() != null) {
          builder.characters(PermittedCharacters.BLIP_TEXT.coerceString(tokenizer.getData()));
          offset += tokenizer.getData().length();
        }
        break;
      case STYLE_FONT_WEIGHT_START:
        startAnnotation(builder, AnnotationConstants.STYLE_FONT_WEIGHT, tokenizer.getData());
        break;
      case STYLE_FONT_WEIGHT_END:
        endAnnotation(builder, AnnotationConstants.STYLE_FONT_WEIGHT);
        break;
      case STYLE_FONT_STYLE_START:
        startAnnotation(builder, AnnotationConstants.STYLE_FONT_STYLE, tokenizer.getData());
        break;
      case STYLE_FONT_STYLE_END:
        endAnnotation(builder, AnnotationConstants.STYLE_FONT_STYLE);
        break;
      case STYLE_TEXT_DECORATION_START:
        startAnnotation(builder, AnnotationConstants.STYLE_TEXT_DECORATION, tokenizer.getData());
        break;
      case STYLE_TEXT_DECORATION_END:
        endAnnotation(builder, AnnotationConstants.STYLE_TEXT_DECORATION);
        break;
      case STYLE_COLOR_START:
        startAnnotation(builder, AnnotationConstants.STYLE_COLOR, tokenizer.getData());
        break;
      case STYLE_COLOR_END:
        endAnnotation(builder, AnnotationConstants.STYLE_COLOR);
        break;
      case STYLE_BG_COLOR_START:
        startAnnotation(builder, AnnotationConstants.STYLE_BG_COLOR, tokenizer.getData());
        break;
      case STYLE_BG_COLOR_END:
        endAnnotation(builder, AnnotationConstants.STYLE_BG_COLOR);
        break;
      case STYLE_FONT_FAMILY_START:
        startAnnotation(builder, AnnotationConstants.STYLE_FONT_FAMILY, tokenizer.getData());
        break;
      case STYLE_FONT_FAMILY_END:
        endAnnotation(builder, AnnotationConstants.STYLE_FONT_FAMILY);
        break;
      case LINK_START:
        startAnnotation(builder, AnnotationConstants.LINK_MANUAL, tokenizer.getData());
        break;
      case LINK_END:
        endAnnotation(builder, AnnotationConstants.LINK_MANUAL);
        break;
      case UNORDERED_LIST_START:
      case ORDERED_LIST_START:
        push(currentType);
        break;
      case UNORDERED_LIST_END:
      case ORDERED_LIST_END:
        pop();
        break;
      default:
        throw new IllegalStateException("Unhandled token: " +
            currentType.toString());
    }
  }

  private void push(Type type) {
    structureStack.add(type);
    indentationLevel += type.indent();
  }

  private void pop() {
    if (structureStack.size() > 0) {
      indentationLevel -= structureStack.remove(structureStack.size() - 1).indent();
    }
  }

  private Type peek() {
    if (structureStack.size() == 0) {
      return null;
    }
    return structureStack.get(structureStack.size() - 1);
  }

  private void startAnnotation(Nindo.Builder builder, String annotationKey,
      String annotationValue) {
    Stack<String> annotationStack = startedAnnotations.get(annotationKey);
    if (annotationStack == null) {
      annotationStack = new Stack<String>();
      startedAnnotations.put(annotationKey, annotationStack);
      affectedKeys.add(annotationKey);
    }
    String current = annotationStack.isEmpty() ? null : annotationStack.peek();
    // Avoid no-ops
    if (ValueUtils.notEqual(annotationValue, current)) {
      if (current == null && isDefaultValue(annotationKey, annotationValue)) {
        // If the current annotation is the default, and the new annotation
        // value is also the default, we don't need to set the annotation value
      } else {
        builder.startAnnotation(annotationKey, annotationValue);
      }
    }
    annotationStack.push(annotationValue);
  }

  private void endAnnotation(Nindo.Builder builder, String annotationKey) {
    Stack<String> annotationStack = startedAnnotations.get(annotationKey);
    Preconditions.checkNotNull(annotationStack, "cannot end unstarted annotation");

    String current = annotationStack.pop();

    // If there are no more entries, and the current annotation is non-null.
    if (annotationStack.isEmpty() && current != null) { // avoid no-ops
      if (!isDefaultValue(annotationKey, current)) {
        builder.endAnnotation(annotationKey);
      }

      // Conditionally null out certain keys if they are in a special list
      if (defaultValueMap.containsKey(annotationKey)) {
        builder.startAnnotation(annotationKey, defaultValueMap.get(annotationKey));
      } else {
        startedAnnotations.remove(annotationKey);
      }
    } else {
      String nextAnnotation = annotationStack.peek();
      if (ValueUtils.notEqual(current, nextAnnotation)) {
        // There's another entry in the stack and it is different from the
        // current-
        // If the entry in the stack is the same as default, just end the annotation.
        // Otherwise, start the next one.
        if (isDefaultValue(annotationKey, nextAnnotation)) {
          builder.endAnnotation(annotationKey);
        } else {
          builder.startAnnotation(annotationKey, nextAnnotation);
        }
      }
    }
  }

  private void startElement(Nindo.Builder builder, String tagName,
      Attributes attributes) {
    ++elementCount;
    builder.elementStart(tagName, attributes);
    ++offset;
  }

  private void endElement(Nindo.Builder builder) {
    if (--elementCount < 0) {
      throw new IllegalStateException("Element count is negative.");
    }
    builder.elementEnd();
    ++offset;
  }

  // TODO(user): Consider and handle the case where the current container might
  // not be a line container (i.e. inside a caption)
  private <N, E extends N, T extends N> void handleNewLine(RichTextTokenizer tokenizer,
      Nindo.Builder builder, ReadableDocument<N, E, T> doc, N splitContainer,
      Attributes attributes) {

    // HACK(user): splitContainer is the container at insertion point. We
    // only want to append new lines if we are pasting into a line container.
    // There are more cases, i.e. if we are pasting in a caption, but that
    // doesn't happen at the moment. This catches most cases, and the problem
    // should go away when we stop using Nindo.Builder and use
    // mutable document instead.
    if (lcCanHandleNewLine(doc, splitContainer)) {
      lcHandleNewLine(tokenizer, builder, doc, splitContainer, attributes);
    }
  }

  private <N, E extends N, T extends N> void lcHandleNewLine(RichTextTokenizer tokenizer,
      Nindo.Builder builder, ReadableDocument<N, E, T> doc, N splitContainer,
      Attributes attributes) {
    // TODO(user): Don't create a new paragraph if the attributes are the same,
    // and is first token.
    boolean isLastToken = !tokenizer.hasNext();
    if ((!isFirstToken && !isLastToken) || !attributes.isEmpty()) {
      startElement(builder, LineContainers.LINE_TAGNAME, attributes);
      endElement(builder);
    }
  }

  private <N, E extends N, T extends N> boolean lcCanHandleNewLine(ReadableDocument<N, E, T> doc,
      N container) {
    return LineContainers.isLineContainer(doc, Point.enclosingElement(doc, container));
  }

  private boolean isDefaultValue(String key, String value) {
    return defaultAnnotations.containsKey(key) && defaultAnnotations.get(key).contains(value);
  }
}
TOP

Related Classes of org.waveprotocol.wave.model.richtext.RichTextMutationBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.