/**
* 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.wave.client.editor.extract;
import org.waveprotocol.wave.client.editor.content.misc.CaretAnnotations;
import org.waveprotocol.wave.client.editor.content.misc.CaretAnnotations.AnnotationResolver;
import org.waveprotocol.wave.model.document.util.DocProviders;
import junit.framework.TestCase;
import org.waveprotocol.wave.model.document.AnnotationBehaviour;
import org.waveprotocol.wave.model.document.AnnotationBehaviour.AnnotationFamily;
import org.waveprotocol.wave.model.document.AnnotationBehaviour.BiasDirection;
import org.waveprotocol.wave.model.document.AnnotationBehaviour.ContentType;
import org.waveprotocol.wave.model.document.AnnotationBehaviour.DefaultAnnotationBehaviour;
import org.waveprotocol.wave.model.document.MutableDocument;
import org.waveprotocol.wave.model.document.RangedAnnotation;
import org.waveprotocol.wave.model.document.operation.Nindo;
import org.waveprotocol.wave.model.document.raw.impl.Element;
import org.waveprotocol.wave.model.document.raw.impl.Node;
import org.waveprotocol.wave.model.document.raw.impl.Text;
import org.waveprotocol.wave.model.document.util.AnnotationRegistry;
import org.waveprotocol.wave.model.document.util.AnnotationRegistryImpl;
import org.waveprotocol.wave.model.document.util.LineContainers;
import org.waveprotocol.wave.model.util.CollectionUtils;
import org.waveprotocol.wave.model.util.ReadableStringMap;
import org.waveprotocol.wave.model.util.ReadableStringMap.ProcV;
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.List;
/**
* Tests a number of cases for the logic of when to replace / inherit annotations while pasting.
*
* NOTE(patcoleman):
* a) Rich text paste is all that's done so far, so that's all that's currently tested
* b) The paste extractor doesn't currently respect right cursor bias.
*/
public class PasteAnnotationLogicTest extends TestCase {
private AnnotationRegistry REGISTRY = null;
private static final String LINK_KEY = "link";
private static final String STYLE_KEY = "style";
private static final String SPELL_KEY = "spell";
/** Sets up a state with anything needed by all the tests. */
private AnnotationRegistry initTestAnnotationBehaviour(AnnotationRegistry root) {
LineContainers.setTopLevelContainerTagname("body");
/**
* Create a registry that has three types of behaviours:
* - style-like (no bias change, inherits inside)
* - link-like (bias away, with priority overriding containers).
* - spell-like (metadata, never inherits)
*/
AnnotationRegistry registry = root.createExtension();
registry.registerBehaviour(STYLE_KEY, new DefaultAnnotationBehaviour(AnnotationFamily.CONTENT));
registry.registerBehaviour(LINK_KEY, new AnnotationBehaviour() {
public BiasDirection getBias(StringMap<Object> left, StringMap<Object> right,
CursorDirection cursor) {
assert left.containsKey(LINK_KEY) && right.containsKey(LINK_KEY);
return left.get(LINK_KEY) == null ? BiasDirection.LEFT : BiasDirection.RIGHT; // away
}
public double getPriority() {
return 10.0; // higher than default
}
public InheritDirection replace(StringMap<Object> inside, StringMap<Object> outside,
ContentType type) {
return InheritDirection.INSIDE;
}
public AnnotationFamily getAnnotationFamily() {
return AnnotationFamily.CONTENT;
}
});
registry.registerBehaviour(SPELL_KEY, AnnotationBehaviour.REPLACE_NEITHER);
useCaretAnnotations();
return registry;
}
private final PasteAnnotationLogic<Node, Element, Text> initDefault() {
REGISTRY = initTestAnnotationBehaviour(AnnotationRegistryImpl.ROOT);
return new PasteAnnotationLogic<Node, Element, Text>(doc, REGISTRY);
}
//
// TESTS
//
// strip keys - paste into middle of normal region
public void testStripKeysInUnstyled() {
useDocument("<line/>abcd");
PasteAnnotationLogic<Node, Element, Text> logic = initDefault();
final StringMap<String> built = CollectionUtils.createStringMap();
Nindo.Builder builder = new Nindo.Builder() {
@Override public void startAnnotation(String key, String value) {
built.put(key, value);
}
};
final StringMap<String> changed =
logic.stripKeys(doc, 5, BiasDirection.LEFT, ContentType.RICH_TEXT, builder);
checkEquals(built, changed);
}
// strip keys - paste into middle of bold region
public void testStripKeysInStyled() {
useDocument("<line/>abcd", "style:S:4:6");
PasteAnnotationLogic<Node, Element, Text> logic = initDefault();
final StringMap<String> built = CollectionUtils.createStringMap();
Nindo.Builder builder = new Nindo.Builder() {
@Override public void startAnnotation(String key, String value) {
built.put(key, value);
}
};
final StringMap<String> changed =
logic.stripKeys(doc, 5, BiasDirection.LEFT, ContentType.RICH_TEXT, builder);
checkEquals(built, changed, "style", null);
}
// strip keys - paste onto divider with normal on right, bold on left
public void testStripKeysBetweenUnstyledStyled() {
useDocument("<line/>abcd", "style:S:5:7");
PasteAnnotationLogic<Node, Element, Text> logic = initDefault();
final StringMap<String> built = CollectionUtils.createStringMap();
Nindo.Builder builder = new Nindo.Builder() {
@Override public void startAnnotation(String key, String value) {
built.put(key, value);
}
};
final StringMap<String> changed =
logic.stripKeys(doc, 5, BiasDirection.LEFT, ContentType.RICH_TEXT, builder);
checkEquals(built, changed);
}
// strip keys - paste onto divider with normal on right, bold on left
public void testStripKeysBetweenStyledUnstyled() {
useDocument("<line/>abcd", "style:S:3:5", "spell:S:2:6");
PasteAnnotationLogic<Node, Element, Text> logic = initDefault();
final StringMap<String> built = CollectionUtils.createStringMap();
Nindo.Builder builder = new Nindo.Builder() {
@Override public void startAnnotation(String key, String value) {
built.put(key, value);
}
};
final StringMap<String> changed =
logic.stripKeys(doc, 5, BiasDirection.LEFT, ContentType.RICH_TEXT, builder);
checkEquals(built, changed, "style", null, "spell", null);
}
// unstrip keys - cover everything (both skipped and unskipped keys)
public void testUnstripKeys() {
useDocument("<line/>abcd", "link:L:4:6", "spell:S:5:7");
PasteAnnotationLogic<Node, Element, Text> logic = initDefault();
final StringSet ended = CollectionUtils.createStringSet();
Nindo.Builder builder = new Nindo.Builder() {
@Override public void endAnnotation(String key) {
ended.add(key);
}
};
ReadableStringSet keyCheck = CollectionUtils.newStringSet("A", "B", "C");
ReadableStringSet toIgnore = CollectionUtils.newStringSet("B", "D");
logic.unstripKeys(builder, keyCheck, toIgnore);
assertEquals(2, ended.countEntries());
assertTrue(ended.contains("A"));
assertFalse(ended.contains("B"));
assertTrue(ended.contains("C"));
assertFalse(ended.contains("D"));
}
// extract normalized - check simple case with link and spell
public void testExtractNormalizedAnnotation() {
// a[b{c]d} where [] = link, {} = spell annotation, normalizing over abcd
useDocument("<line/>abcd", "link:L:4:6", "spell:S:5:7", "style:Q:1:2");
PasteAnnotationLogic<Node, Element, Text> logic = initDefault();
List<RangedAnnotation<String>> extracted =
logic.extractNormalizedAnnotation(doc.locate(3), doc.locate(7));
assertEquals(4, extracted.size());
checkRangedAnnotation(extracted, 0, 4, STYLE_KEY, null); // no styles
checkRangedAnnotation(extracted, 0, 1, LINK_KEY, null); // before linked bit
checkRangedAnnotation(extracted, 1, 3, LINK_KEY, "L"); // linked bit
checkRangedAnnotation(extracted, 3, 4, LINK_KEY, null); // after linked bit
}
//
// Utilities
//
// CHeck whether A = B, and both contain mappings (key = 2n, value = 2n+1);
private void checkEquals(ReadableStringMap<String> A, ReadableStringMap<String> B, String... C) {
StringMap<String> shouldBe = CollectionUtils.createStringMap();
for (int i = 0; i < C.length; i += 2) {
shouldBe.put(C[i], C[i+1]);
}
checkMapEquals(A, shouldBe);
checkMapEquals(B, shouldBe);
}
private void checkMapEquals(ReadableStringMap<String> A, final ReadableStringMap<String> B) {
assertEquals(B.countEntries(), A.countEntries());
A.each(new ProcV<String>() {
public void apply(String key, String value) {
assertTrue(B.containsKey(key));
assertTrue(ValueUtils.equal(value, B.get(key)));
}
});
}
private void checkRangedAnnotation(List<RangedAnnotation<String>> extracted,
int from, int to, String key, String value) {
boolean hasAnnotation = true;
for (RangedAnnotation<String> annotation : extracted) {
if (annotation.start() == from && annotation.end() == to &&
ValueUtils.equal(key, annotation.key()) && ValueUtils.equal(value, annotation.value())) {
return;
}
}
fail();
}
///
/// Convenience methods for a context, copied from EditorAnnotationUtilTest.
/// TODO(patcoleman): make it so this doesn't have to be copied everywhere.
///
// Parameters to make generating the EditorContext wrappers easier
private int start;
private int end;
private MutableDocument<Node, Element, Text> doc;
private CaretAnnotations caret;
// Resolve the annotations by left-biasing within the document, stores it in doc.
AnnotationResolver annotationResolver = new AnnotationResolver() {
public String getAnnotation(String key) {
return start == 0 || doc.getAnnotation(start - 1, key) == null ?
null : doc.getAnnotation(start - 1, key).toString();
}
};
// Converts the document and annotations into a CMutableDocument and stores in doc
private void useDocument(String docContent, String... annotations) {
MutableDocument<Node, Element, Text> mutable =
DocProviders.MOJO.parse("<body>" + docContent + "</body>");
// format: key:value:start:end
for (String value : annotations) {
String[] vals = value.split(":");
mutable.setAnnotation(Integer.parseInt(vals[2]), Integer.parseInt(vals[3]), vals[0], vals[1]);
}
doc = mutable;
}
// Converts the annotations passed into a CaretAnnotations bundle and stores in caret
private void useCaretAnnotations(String... annotations) {
CaretAnnotations localCaret = new CaretAnnotations();
for (String value : annotations) {
String[] dup = value.split(":");
localCaret.setAnnotation(dup[0], dup[1]);
}
caret = localCaret;
caret.setAnnotationResolver(annotationResolver);
}
// Stores the start and end of the selection range to simulate.
private void useSelection(int begin, int finish) {
start = begin;
end = finish;
}
}
// PASTE TESTS to write?
// plaintext paste of normal in bold
// richtext paste of normal in bold
// richtext paste of normal on a blank line between bold and normal
// richtext paste of bold on a blank line between bold and normal
// richtext paste of normal + bold on a blank line between bold and normal
// richtext paste of normal at the end of a line after normal
// richtext paste of bold + normal at the end of a line after normal
// richtext paste of normal at the end of a line after bold
// richtext paste of normal + bold at the end of a line after bold