Package org.waveprotocol.wave.model.document.util

Source Code of org.waveprotocol.wave.model.document.util.LineContainers

/**
* 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.document.util;


import org.waveprotocol.wave.model.document.MutableDocument;
import org.waveprotocol.wave.model.document.MutableDocument.Action;
import org.waveprotocol.wave.model.document.ReadableDocument;
import org.waveprotocol.wave.model.document.ReadableWDocument;
import org.waveprotocol.wave.model.document.operation.Attributes;
import org.waveprotocol.wave.model.util.Preconditions;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* Constants and utilities for line containers
*
* TODO(danilatos): This should become a wrapper on the document, with the static
* methods no longer being static, and the tag dependencies being injected.
*
* @author danilatos@google.com (Daniel Danilatos)
*/
public final class LineContainers {

  /**
   * True if line containers should be used - false if still using old
   * paragraphs.
   *
   * NOTE(danilatos): Setting this to true might break
   * AggressiveSelectionHelperTest. It will need updating.
   */
  public static final boolean USE_LINE_CONTAINERS_BY_DEFAULT = true;

  public static final String LINE_TAGNAME = "line";

  public static final String PARAGRAPH_NS = "l";
  public static final String PARAGRAPH_TAGNAME = "p";
  public static final String PARAGRAPH_FULL_TAGNAME = PARAGRAPH_NS + ":" + PARAGRAPH_TAGNAME;

  // TODO(danilatos): Convert this class to no longer be static,
  // and then these would be member variables.
  private static String topLevelContainerTagname;
  private static final Set<String> lineContainerTagnames = new HashSet<String>();

  /**
   * Sets the tag name for the top level line container
   *
   * MUST be set before attempting to append a line to an empty document
   *
   * @param tagName
   */
  public static void setTopLevelContainerTagname(String tagName) {
    Preconditions.checkNotNull(tagName, "Top level tag name must not be null");
    topLevelContainerTagname = tagName;
    registerLineContainerTagname(tagName);
  }

  /**
   * Default tag name for the top level line container
   *
   * Implicitly created when appending lines to empty documents
   */
  public static String topLevelContainerTagname() {
    Preconditions.checkState(topLevelContainerTagname != null,
        "Top level line container tag name not set!");
    return topLevelContainerTagname;
  }

  /**
   * Register a tag name as a line container, to recognise all
   * elements with that tag name as being able to contain line elements.
   *
   * @param tagName
   */
  public static void registerLineContainerTagname(String tagName) {
    lineContainerTagnames.add(tagName);
  }

  /**
   * Defines a text rounding granularity
   */
  // TODO(danilatos/mtsui): Should this be unified with either of the two MoveUnit enums?
  // This currently does not include any display logic (such as visual line vs logical line,
  // or page), which is a bit different to the other move units, which contain both visual
  // and logical units.
  public enum Rounding {
    /** No rounding */
    NONE,
    /** Round to word boundary */
    WORD,
    /** Round to sentence boundary */
    SENTENCE,
    /** Round to line boundary */
    LINE
  }

  /**
   * Defines in which direction rounding is to be applied.
   */
  public enum RoundDirection {
    LEFT,
    RIGHT
  }

  /**
   * @param doc
   * @param rounding
   * @param location
   * @param direction Whether the rounding goes leftwards or rightwards
   * @return the given location rounded rightwards to the requested granularity
   */
  public static <N, E extends N, T extends N> Point<N> roundLocation(
      ReadableDocument<N, E, T> doc, Rounding rounding, Point<N> location,
      RoundDirection direction) {
    Preconditions.checkNotNull(direction, "Rounding direction cannot be null.");

    switch (rounding) {
      case NONE:
        return location;
      case WORD:
      case SENTENCE:
        // TODO(mtsui/danilatos): Use and/or unify with TextLocator
        throw new UnsupportedOperationException("Not implemented");
      case LINE:
        checkNotParagraphDocument(doc);

        Point<N> point = jumpOutToContainer(doc, location);
        if (point == null) {
          return null;
        }

        E el = Point.enclosingElement(doc, point);
        if (direction == RoundDirection.RIGHT) { // round to the right
          N nodeAfter = point.isInTextNode()
              ? doc.getNextSibling(point.getContainer()) : point.getNodeAfter();
          while (nodeAfter != null && !isLineElement(doc, nodeAfter)) {
            nodeAfter = doc.getNextSibling(nodeAfter);
          }
          return Point.<N>inElement(el, nodeAfter);
        } else { // otherwise, round left (backwards)
          N nodeBefore = point.isInTextNode() ? doc.getPreviousSibling(point.getContainer())
              : Point.nodeBefore(doc, point.asElementPoint());
          while (nodeBefore != null && !isLineElement(doc, nodeBefore)) {
            nodeBefore = doc.getPreviousSibling(nodeBefore);
          }
          return nodeBefore == null ? null : Point.before(doc, nodeBefore);
        }
      default:
        throw new AssertionError("Missing rounding implementations");
    }
  }

  /**
   * Predicates a node being a line container
   */
  public static final DocPredicate LINE_CONTAINER_PREDICATE = new DocPredicate() {
    @Override
    public <N, E extends N, T extends N> boolean apply(ReadableDocument<N, E, T> doc, N node) {
      return isLineContainer(doc, node);
    }
  };

  /**
   * Jumps the point out to the enclosing line container, if any
   *
   * @see DocHelper#jumpOut(ReadableDocument, Point, DocPredicate)
   */
  public static <N, E extends N, T extends N> Point<N> jumpOutToContainer(
      ReadableDocument<N, E, T> doc, Point<N> location) {
    return DocHelper.jumpOut(doc, location, LINE_CONTAINER_PREDICATE);
  }

  /**
   * Finds the last line element that is before a given location, which should be within a
   * line container element.
   *
   * @param doc
   * @param at
   * @return The line element or null if not found.
   */
  public static <N, E extends N, T extends N> E getRelatedLineElement(
      ReadableDocument<N, E, T> doc, Point<N> at) {
    Point<N> atStart = roundLocation(doc, Rounding.LINE, at, RoundDirection.LEFT);

    // atStart should now have the lineContainer as the parent and the line element as nodeAfter:
    if (atStart == null || atStart.getNodeAfter() == null) {
      return null; // nothing found
    }

    return doc.asElement(atStart.getNodeAfter());
  }

  /**
   * @param doc
   * @param point
   * @return true if the given location is at the end of a line
   */
  public static <N, E extends N, T extends N> boolean isAtLineEnd(
      ReadableWDocument<N, E, T> doc, Point<N> point) {
    return doc.getLocation(point) == doc.getLocation(LineContainers.roundLocation(
        doc, Rounding.LINE, point, RoundDirection.RIGHT));
  }

  /**
   * @param doc
   * @param point
   * @return true if the given location is at the start of a line
   */
  public static <N, E extends N, T extends N> boolean isAtLineStart(
      ReadableWDocument<N, E, T> doc, Point<N> point) {
    E elementBefore = point == null ? null : Point.elementBefore(doc, point);
    return elementBefore != null ? isLineElement(doc, elementBefore) : false;
  }

  /**
   * @param doc
   * @param point
   * @return true if the given location is at an empty line
   */
  public static <N, E extends N, T extends N> boolean isAtEmptyLine(
      ReadableWDocument<N, E, T> doc, Point<N> point) {
    return isAtLineStart(doc, point) && isAtLineEnd(doc, point);
  }

  /**
   * Inserts content into a point that is within a line. If the point is not
   * within a line, will create a new one at the next available location.
   *
   * @param doc the document to insert into.
   * @param point the point within a line to insert.
   * @param content the content to insert.
   * @return the node that was inserted into.
   */
  public static <N, E extends N, T extends N> N insertInto(MutableDocument<N, E, T> doc,
      Point<N> point, XmlStringBuilder content) {

    checkNotParagraphDocument(doc);

    E lc = null;
    for (E el : DocIterate.deepElementsReverse(doc, doc.getDocumentElement(), null)) {
      if (isLineContainer(doc, el)) {
        lc = el;
        break;
      }
    }
    if (lc != null) {
      // This garbage code attempts to figure out if the current location
      // is after a line declaration. Has to be an easier way, but this is
      // quick and dirty.
      int location = doc.getLocation(point);
      // Find the first line.
      for (N child = doc.getFirstChild(lc); child != null; child = doc.getNextSibling(lc)) {
        if (isLineElement(doc, child)) {
          if (doc.getLocation(child) < location) {
            return doc.insertXml(point, content);
          }
        }
      }
    }

    // Just insert a line here.
    return insertContentOnNewLine(doc, Rounding.NONE, point, content);
  }

  /**
   * Deletes a line inside of a line container. Takes care to not invalidate
   * the schema by leaving an empty line container. If the line to be deleted
   * is the last one, then it will be emptied and left alone instead.
   *
   * @param doc
   * @param line the element marking the start of the line to remove.
   */
  public static <N, E extends N, T extends N> void deleteLine(MutableDocument<N, E, T> doc,
      E line) {
    checkNotParagraphDocument(doc);
    if (!isLineElement(doc, line)) {
      Preconditions.illegalArgument("Not a line element: " + line);
    }

    E lc = doc.getParentElement(line);
    if (!isLineContainer(doc, lc)) {
      Preconditions.illegalArgument("Not a line container: " + lc);
    }

    boolean isFirstLine = doc.getFirstChild(lc) == line;

    Point<N> deleteEndPoint =
        roundLocation(doc, Rounding.LINE, Point.after(doc, line), RoundDirection.RIGHT);
    // If this is not the first line or there is another line, then we can
    // delete this one. Otherwise, empty it and leave it.
    Point<N> deleteStartPoint = null;
    if (!isFirstLine || isLineElement(doc, deleteEndPoint.getNodeAfter())) {
      deleteStartPoint = Point.before(doc, line);
    } else {
      doc.emptyElement(line);
      deleteStartPoint = Point.after(doc, line);
    }

    doc.deleteRange(deleteStartPoint, deleteEndPoint);
  }

  /**
   * For a given document, will linearly scan all lines and return a list of
   * ranges representing each. The start point of each range will be the first
   * point after the end line element and the end point will be the point
   * before the next line (or before the end tag of the line container in the
   * case of the last line).
   *
   * @param doc
   * @return list of ranges representing each line.
   */
  public static <N, E extends N, T extends N> List<Range> getLineRanges(
      MutableDocument<N, E, T> doc) {
    checkNotParagraphDocument(doc);

    List<Range> lines = new ArrayList<Range>();
    N root = doc.getDocumentElement();
    for (N lc = doc.getFirstChild(root); lc != null; lc = doc.getNextSibling(lc)) {
      if (isLineContainer(doc, lc)) {
        int start = -1;
        for (N line = doc.getFirstChild(lc); line != null; line = doc.getNextSibling(line)) {
          if (isLineElement(doc, line)) {
            if (start > 0) {
              int end = doc.getLocation(Point.before(doc, line));
              lines.add(new Range(start, end));
            }
            start = doc.getLocation(Point.after(doc, line));
          }
        }
        if (start > 0) {
          lines.add(new Range(start, doc.getLocation(Point.end(lc))));
        }
      }
    }

    return lines;
  }

  /**
   * Inserts a line at the given location
   *
   * @param doc
   * @param rounding rightwards rounding to apply to the given location
   * @param location
   * @return the new line element
   *
   * Temporarily supports paragraphs as well
   */
  public static <N, E extends N, T extends N> E insertLine(final MutableDocument<N, E, T> doc,
      Rounding rounding, Point<N> location) {
    return insertLine(doc, rounding, location, Attributes.EMPTY_MAP);
  }

  /**
   * Inserts a line at the given location
   *
   * @param doc
   * @param rounding rightwards rounding to apply to the given location
   * @param location
   * @param attributes
   * @return the new line element
   *
   * Temporarily supports paragraphs as well
   */
  public static <N, E extends N, T extends N> E insertLine(final MutableDocument<N, E, T> doc,
      Rounding rounding, Point<N> location, Attributes attributes) {

    Preconditions.checkNotNull(rounding, "rounding must not be null");

    location = roundLocation(doc, rounding, location, RoundDirection.RIGHT);
    Preconditions.checkArgument(location != null, "location is not a valid place to insert a line");

    checkNotParagraphDocument(doc);

    // Make sure this is a valid place to insert the line, even if it means
    // dishonouring the rounding. Line rounding should already have done this.
    if (rounding != Rounding.LINE) {
      location = jumpOutToContainer(doc, location);
    }

    return doc.createElement(location, LINE_TAGNAME, attributes);
  }

  public static <N, E extends N, T extends N> N insertContentOnNewLine(
      final MutableDocument<N, E, T> doc,
      Rounding rounding, Point<N> location, XmlStringBuilder initialContent) {
    return insertContentIntoLineStart(doc, insertLine(doc, rounding, location), initialContent);
  }

  public static <N, E extends N, T extends N> N appendContentOnNewLine(
      MutableDocument<N, E, T> doc, XmlStringBuilder initialContent) {
    // TODO(user): This is redundant to appendLine with content. Remove.
    return insertContentIntoLineStart(doc, appendLine(doc, null), initialContent);
  }

  /**
   * Inserts content into the end of the line specified by the element.
   *
   * @param doc
   * @param line the line element to insert into
   * @param content the content to insert
   * @return the node that was inserted into.
   */
  public static <N, E extends N, T extends N> N insertContentIntoLineEnd(
      final MutableDocument<N, E, T> doc, E line, XmlStringBuilder content) {
    // Find the next line and insert just before it.
    Point<N> point = roundLocation(doc, Rounding.LINE, Point.start(doc, line),
        RoundDirection.RIGHT);
    if (point == null) {
      throw new AssertionError("Not a valid line location.");
    }
    return doc.insertXml(point, content);
  }

  /**
   * Inserts content into the start of the line specified by the element.
   *
   * @param doc
   * @param line the line element to insert into
   * @param initialContent the content to insert
   * @return the node that was inserted.
   */
  public static <N, E extends N, T extends N> N insertContentIntoLineStart(
      final MutableDocument<N, E, T> doc, E line, XmlStringBuilder initialContent) {
    doc.insertXml(Point.after(doc, line), initialContent);
    return doc.getNextSibling(line);
  }

  public static void properAppendLine(final MutableDocument<?, ?, ?> doc,
      final XmlStringBuilder content) {
    doc.with(new Action() {
      @Override
      public <N, E extends N, T extends N> void exec(MutableDocument<N, E, T> doc) {
        appendLine(doc, content);
      }
    });
  }

  public static <N, E extends N, T extends N> E appendLine(MutableDocument<N, E, T> doc,
      XmlStringBuilder content) {
    return appendLine(doc, content, Attributes.EMPTY_MAP);
  }

  /**
   * Appends a line to the last line container of the document
   *
   * If the document has no line containers, one will be created at the end of
   * the document.
   *
   * Temporarily also supports old style paragraphs for old documents, in which
   * case the new paragraph is returned
   *
   * @param doc
   * @param content optional content for the new line, may be null
   * @return the line token representing the start of the new line
   */
  public static <N, E extends N, T extends N> E appendLine(final MutableDocument<N, E, T> doc,
      XmlStringBuilder content, Attributes attributes) {

    checkNotParagraphDocument(doc);

    E lc = null;
    for (E el : DocIterate.deepElementsReverse(doc, doc.getDocumentElement(), null)) {
      if (isLineContainer(doc, el)) {
        lc = el;
        break;
      }
    }

    if (lc == null) {
      // Create the <body><line></line></body> in one go.
      lc = doc.appendXml(XmlStringBuilder.createEmpty().wrap(LINE_TAGNAME).wrap(
          topLevelContainerTagname()));

      // Add the content before </body>
      if (content != null && content.getLength() > 0) {
        doc.insertXml(Point.<N>end(lc), content);
      }
      E line = doc.asElement(doc.getFirstChild(lc));
      assert line != null;
      if (attributes != null) {
        doc.setElementAttributes(line, attributes);
      }
      return line;
    } else {
      return appendLine(doc, lc, content, attributes);
    }
  }

  /**
   * Finds the last valid line and appends to the end of it.
   *
   * @param doc the document to insert into.
   * @param content the content to append.
   */
  public static <N, E extends N, T extends N> E appendToLastLine(MutableDocument<N, E, T> doc,
      XmlStringBuilder content) {
    checkNotParagraphDocument(doc);

    // TODO(user): Don't duplicate the code below.
    for (E el : DocIterate.deepElementsReverse(doc, doc.getDocumentElement(), null)) {
      if (isLineContainer(doc, el)) {
        // TODO(user): Check for at least a line tag? I'm assuming
        // there is one...
        Point<N> point = Point.inElement((N) el, null);
        if (point != null) {
          return doc.insertXml(point, content);
        }
      }
    }

    // Looks like no line to add to, just append.
    return appendLine(doc, content);
  }

  public static <N, E extends N, T extends N> E appendLine(final MutableDocument<N, E, T> doc,
      E lineContainer, XmlStringBuilder content) {
    return appendLine(doc, lineContainer, content, Attributes.EMPTY_MAP);
  }

  /**
   * Appends a line to the given line container
   *
   * @param doc
   * @param lineContainer
   * @param content optional content for the new line, may be null
   * @param attributes optional attributes, may be null.
   * @return the line token representing the start of the new line
   */
  public static <N, E extends N, T extends N> E appendLine(final MutableDocument<N, E, T> doc,
      E lineContainer, XmlStringBuilder content, Attributes attributes) {
    E line = doc.createChildElement(lineContainer, LINE_TAGNAME, attributes);
    if (content != null && content.getLength() > 0) {
      doc.insertXml(Point.<N>end(lineContainer), content);
    }
    return line;
  }

  /**
   * Returns true iff the tagname is a line container.
   *
   *         NOTE(danilatos): In the future, the match may involve more than
   *         just a tag name check. Other element types, such as table cells,
   *         might be line containers.
   *
   * @param tagname tagname to check
   * @return true iff the tagname is a line container
   */
  public static boolean isLineContainerTagname(String tagname) {
    return lineContainerTagnames.contains(tagname);
  }

  /**
   * @param doc
   * @return true if the given document is an old-style-paragraph document
   */
  @Deprecated
  private static <N, E extends N, T extends N> boolean isUnsupportedParagraphDocument(
      ReadableDocument<N, E, T> doc) {
    if (doc.getFirstChild(doc.getDocumentElement()) == null) {
      // If the document is empty, check what the default global option is
      return !USE_LINE_CONTAINERS_BY_DEFAULT;
    }
    // Testing all children in the case of special <input> tags
    N root = doc.getDocumentElement();
    for (N child = doc.getFirstChild(root); child != null; child = doc.getNextSibling(child)) {
      if (isUnsupportedParagraphElement(doc, child)) {
        return true;
      }
    }
    return false;
  }

  /** For temporary assertion purposes */
  public static <N, E extends N, T extends N> void checkNotParagraphDocument(
      ReadableDocument<N, E, T> doc) {
    Preconditions.checkArgument(!isUnsupportedParagraphDocument(doc),
        "Paragraph docs no longer supported");
  }

  /**
   * @param doc
   * @param node
   * @return true if the node is a line container element
   *
   *         NOTE(danilatos): In the future, the match may involve more than
   *         just a tag name check. Other element types, such as table cells,
   *         might be line containers.
   */
  public static <N, E extends N> boolean isLineContainer(
      final ReadableDocument<N, E, ?> doc, N node) {
    E el = doc.asElement(node);
    if (el != null) {
      return isLineContainerTagname(doc.getTagName(el));
    } else {
      return false;
    }
  }

  /**
   * @param doc
   * @param node
   * @return true if the node is a line token element
   */
  public static <N, E extends N> boolean isLineElement(
      final ReadableDocument<N, E, ?> doc, N node) {
    return DocHelper.isMatchingElement(doc, node, LINE_TAGNAME);
  }

  /**
   * @param doc
   * @param element a line element
   * @return true if the element is the first line in the document
   */
  public static <N, E extends N> boolean isFirstLine(
      final ReadableDocument<N, E, ?> doc, E element) {
    Preconditions.checkArgument(isLineElement(doc, element), "not a line element");
    return DocHelper.getPreviousSiblingElement(doc, element) == null;
  }

  /** to be deleted */
  @Deprecated
  public static <N, E extends N> boolean isUnsupportedParagraphElement(
      final ReadableDocument<N, E, ?> doc, N node) {
    return DocHelper.isMatchingElement(doc, node, PARAGRAPH_TAGNAME);
  }

  /**
   * Used for testing purposes, wraps content with correct tags.
   *
   * @param lines the lines to wrap.
   * @return the wrapped content.
   */
  public static String debugLineWrap(String ... lines) {
    StringBuilder body = new StringBuilder();
    for (String line : lines) {
      body.append("<" + LINE_TAGNAME + "></" + LINE_TAGNAME + ">" + line);
    }
    return body.toString();
  }

  /**
   * Used for testing purposes, wraps content with correct tags.
   *
   * @param lines the lines to wrap. if null, will not add a new line.
   * @return the wrapped content.
   */
  public static String debugContainerWrap(String ... lines) {
    return "<" + topLevelContainerTagname + ">" + debugLineWrap(lines)
        + "</" + topLevelContainerTagname + ">";
  }

  private LineContainers() {
  }
}
TOP

Related Classes of org.waveprotocol.wave.model.document.util.LineContainers

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.