/** * Copyright 2009 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.model.richtext; import junit.framework.TestCase; import org.waveprotocol.wave.model.document.ReadableWDocument; import org.waveprotocol.wave.model.document.indexed.IndexedDocument; import org.waveprotocol.wave.model.document.operation.Nindo; import org.waveprotocol.wave.model.document.raw.RawDocumentProviderImpl; 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.RawDocumentImpl; import org.waveprotocol.wave.model.document.raw.impl.Text; import org.waveprotocol.wave.model.document.util.DocCompare; import org.waveprotocol.wave.model.document.util.DocProviders; import org.waveprotocol.wave.model.document.util.ElementStyleView; import org.waveprotocol.wave.model.document.util.LineContainers; import org.waveprotocol.wave.model.document.util.Point; import org.waveprotocol.wave.model.document.util.RawElementStyleView; import org.waveprotocol.wave.model.operation.OperationException; import org.waveprotocol.wave.model.richtext.RichTextTokenizerImpl.Token; import org.waveprotocol.wave.model.util.Pair; /** * Tests logic of the rich text mutation builder. * */ public class RichTextMutationBuilderTest extends TestCase { @Override protected void setUp() throws Exception { LineContainers.setTopLevelContainerTagname("body"); } public void testInline() { verifyMutations("{++\"foo\"; }", buildTokens( token(RichTextTokenizer.Type.TEXT, "foo"))); verifyMutations("{++\"foo\"; ++\"bar\"; }", buildTokens( token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.TEXT, "bar"))); } public void testSingleLine() { // This tests a special case in pasting single paragraphs. // Because a cursor is always in a <p> tag within the editor, // we don't want to close it and start a new one when pasting // <p>foo</p>. So we ignore the first and last newline, otherwise // pasting naively would result in something like: // <p></p> // <p>foo</p> // <p>|</p> verifyMutationsLC("{++\"foo\"; }", buildTokens(token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE))); } public void testMultipleLines() { verifyMutationsLC("{++\"foo\"; << line {}; >>; ++\"bar\"; }", buildTokens(token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "bar"), token(RichTextTokenizer.Type.NEW_LINE))); verifyMutationsLC("{++\"one\"; << line {}; >>; ++\"two\"; << line {}; >>; ++\"three\"; }", buildTokens(token(RichTextTokenizer.Type.TEXT, "one"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "two"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "three"))); } public void testExtraSpaces() { verifyMutationsLC("{++\"foo\"; << line {}; >>; ++\"bar\"; }", buildTokens(token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "bar"), token(RichTextTokenizer.Type.NEW_LINE))); } public void testSplits() { verifyMutationsLC("{++\"foo\"; << line {}; >>; }", buildTokens(token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.NEW_LINE))); verifyMutationsLC("{++\"foo\"; << line {}; >>; ++\"bar\"; }", buildTokens(token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "bar"), token(RichTextTokenizer.Type.NEW_LINE))); verifyMutationsLC("{++\"foo\"; << line {}; >>; ++\"bar\"; << line {}; >>; ++\"baz\"; }", buildTokens(token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "bar"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "baz"))); } public void testHeading() { verifyMutationsLC("{++\"foo\"; }", buildTokens(token(RichTextTokenizer.Type.NEW_LINE, "h1"), token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE))); } public void testLinks() { verifyMutations("{(( link/manual=\"http://www.google.com/\"; ++\"Goog\"; )) link/manual; }", buildTokens(token(RichTextTokenizer.Type.LINK_START, "http://www.google.com/"), token(RichTextTokenizer.Type.TEXT, "Goog"), token(RichTextTokenizer.Type.LINK_END))); } public void testStyles() { verifyStyle(RichTextTokenizer.Type.STYLE_FONT_WEIGHT_START, RichTextTokenizer.Type.STYLE_FONT_WEIGHT_END, "style/fontWeight", "bold"); verifyStyle(RichTextTokenizer.Type.STYLE_FONT_STYLE_START, RichTextTokenizer.Type.STYLE_FONT_STYLE_END, "style/fontStyle", "italic"); verifyStyle(RichTextTokenizer.Type.STYLE_FONT_FAMILY_START, RichTextTokenizer.Type.STYLE_FONT_FAMILY_END, "style/fontFamily", "Times New Roman"); verifyStyle(RichTextTokenizer.Type.STYLE_COLOR_START, RichTextTokenizer.Type.STYLE_COLOR_END, "style/color", "#FF00FF"); } public void testInvalidStyles() { verifyMutations("{(( style/fontWeight=\"bold\"; ++\"foo\"; )) style/fontWeight; }", buildTokens(token(RichTextTokenizer.Type.STYLE_FONT_WEIGHT_START, "bold"), token(RichTextTokenizer.Type.TEXT, "foo"))); verifyMutations("{(( style/fontWeight=\"bold\"; ++\"foo\"; )) style/fontWeight; }", buildTokens(token(RichTextTokenizer.Type.STYLE_FONT_WEIGHT_START, "bold"), token(RichTextTokenizer.Type.STYLE_FONT_WEIGHT_START, "bold"), token(RichTextTokenizer.Type.TEXT, "foo"))); verifyMutations("{(( style/fontWeight=\"bold\"; )) style/fontWeight; }", buildTokens(token(RichTextTokenizer.Type.STYLE_FONT_WEIGHT_START, "bold"), token(RichTextTokenizer.Type.STYLE_FONT_WEIGHT_END))); } public void testList() { // verifyMutationsLC("{<< line {t=li}; >>; ++\"foo\"; }", // buildTokens(token(RichTextTokenizer.Type.LIST_ITEM), // token(RichTextTokenizer.Type.TEXT, "foo"), // token(RichTextTokenizer.Type.NEW_LINE))); verifyMutationsLC("{<< line {t=li}; >>; ++\"foo\"; << line {i=1}; >>; ++\"nested\"; " + "<< line {t=li}; >>; ++\"bar\"; }", buildTokens( token(RichTextTokenizer.Type.UNORDERED_LIST_START), token(RichTextTokenizer.Type.LIST_ITEM), token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "nested"), token(RichTextTokenizer.Type.LIST_ITEM), token(RichTextTokenizer.Type.TEXT, "bar"), token(RichTextTokenizer.Type.UNORDERED_LIST_END))); verifyMutationsLC("{<< line {t=li}; >>; ++\"foo\"; << line {i=1}; >>; ++\"nested\"; " + "<< line {t=li}; >>; ++\"bar\"; << line {}; >>; ++\"after\"; }", buildTokens( token(RichTextTokenizer.Type.UNORDERED_LIST_START), token(RichTextTokenizer.Type.LIST_ITEM), token(RichTextTokenizer.Type.TEXT, "foo"), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "nested"), token(RichTextTokenizer.Type.LIST_ITEM), token(RichTextTokenizer.Type.TEXT, "bar"), token(RichTextTokenizer.Type.UNORDERED_LIST_END), token(RichTextTokenizer.Type.NEW_LINE), token(RichTextTokenizer.Type.TEXT, "after"))); } /** * Tests that html strings with styling is converted into correct annotations. * NOTE(user): This exercises code in document and tokenizer as well. */ public void testAnnotations() { annotationTest("<?a 'style/fontWeight'='bold'?>abc<?a 'style/fontWeight'?>", "<b>abc</b>"); annotationTest("<?a 'style/fontWeight'='bold'?>abcdef<?a 'style/fontWeight'?>", ("<b>ab<b>c</b>def</b>")); // Test that default styles are ignored. annotationTest("abcdef", "abc<span style='textDecoration: none;'>def</span>"); annotationTest("abcd<?a 'style/textDecoration'='underline'?>e<?a 'style/textDecoration'?>f", "abc<span style='textDecoration: none;'>d<u>e</u>f</span>"); annotationTest( "n" + "<?a 'style/textDecoration'='underline'?>u" + "<?a 'style/textDecoration'='overline'?>o" + "<?a 'style/textDecoration'='none'?>n" + "<?a 'style/textDecoration'='overline'?>o" + "<?a 'style/textDecoration'='underline'?>u" + "<?a 'style/textDecoration'?>n", "n" + "<u>u" + "<span style='textDecoration: overline;'>o" + "<span style='textDecoration: none;'>n" + "</span>o" + "</span>u" + "</u>n"); } private ElementStyleView<Node, Element, Text> parseDocumentContents(String xmlString) { return new RawElementStyleView( RawDocumentProviderImpl.create(RawDocumentImpl.BUILDER).parse( "<div>" + xmlString + "</div>")); } private Pair<Nindo, IndexedDocument<Node, Element, Text>> applyTokensToEmptyDoc( RichTextTokenizer tokens) { IndexedDocument<Node, Element, Text> doc = DocProviders.POJO.parse("<body><line/></body>"); Point<Node> insertAt = doc.locate(3); Nindo.Builder builder = new Nindo.Builder(); builder.skip(3); new RichTextMutationBuilder().applyMutations(tokens, builder, doc, insertAt.getContainer()); Nindo nindo = builder.build(); try { doc.consumeAndReturnInvertible(nindo); } catch (OperationException e) { fail("Operation Exception " + e); } return new Pair<Nindo, IndexedDocument<Node,Element,Text>>(nindo, doc); } /** * Asserts that the expectedContent is produced from srcHtml. * * Single quotes are used as they do not require escaping and makes the * strings more readable. * * @param expectedContent expected content with annotation key/value in single * quotes. * @param srcHtml source html with attributes in single quotes. */ private void annotationTest(String expectedContent, String srcHtml) { // The parser implementation does not accept single quotes, so we must // convert. RichTextTokenizerImpl<Node, Element, Text> tokenizer = new RichTextTokenizerImpl<Node, Element, Text>(parseDocumentContents(srcHtml)); Pair<Nindo, IndexedDocument<Node, Element, Text>> result = applyTokensToEmptyDoc(tokenizer); ReadableWDocument<Node, Element, Text> doc = result.second; DocCompare.equivalent(DocCompare.ALL, expectedContent, result.second); } private void verifyMutations(String opString, RichTextTokenizer tokens) { verifyMutationsLC(opString, tokens); } private void verifyStyle(RichTextTokenizer.Type start, RichTextTokenizer.Type end, String key, String value) { verifyMutations("{(( " + key + "=\"" + value + "\"; ++\"foo\"; )) " + key + "; }", buildTokens(token(start, value), token(RichTextTokenizer.Type.TEXT, "foo"), token(end))); } private void verifyMutationsLC(String opString, RichTextTokenizer tokens) { Pair<Nindo, IndexedDocument<Node, Element, Text>> result = applyTokensToEmptyDoc(tokens); Nindo nindo = Nindo.shift(-3, result.first); if (!opString.equals(nindo.toString())) { System.out.println("ACTUAL: " + nindo); System.out.println("EXPECT: " + opString); throw new AssertionError(opString + ", " + nindo); } assertEquals(opString, nindo.toString()); } private RichTextTokenizer buildTokens(Token ... tokens) { MockRichTextTokenizer mockTokenizer = new MockRichTextTokenizer(); for (Token token : tokens) { mockTokenizer.expectToken(token); } return mockTokenizer; } private static Token token(RichTextTokenizer.Type type, String data) { return new Token(type, data); } private static Token token(RichTextTokenizer.Type type) { return new Token(type, null); } }