test harness for L1 text handling
Signed-off-by: Florian Thienel <florian@thienel.org>
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DummyValidator.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DummyValidator.java
new file mode 100644
index 0000000..64503c9
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/DummyValidator.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.dom;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.core.runtime.QualifiedName;
+import org.eclipse.vex.core.internal.validator.AttributeDefinition;
+
+/**
+ * @author Florian Thienel
+ */
+public class DummyValidator implements Validator {
+
+ public AttributeDefinition getAttributeDefinition(final Attribute attribute) {
+ return null;
+ }
+
+ public List<AttributeDefinition> getAttributeDefinitions(final Element element) {
+ return Collections.emptyList();
+ }
+
+ public Set<QualifiedName> getValidRootElements() {
+ return Collections.emptySet();
+ }
+
+ public Set<QualifiedName> getValidItems(final Element element) {
+ return Collections.emptySet();
+ }
+
+ public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> nodes, final boolean partial) {
+ return false;
+ }
+
+ public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> sequence1, final List<QualifiedName> sequence2, final List<QualifiedName> sequence3, final boolean partial) {
+ return false;
+ }
+
+ public Set<String> getRequiredNamespaces() {
+ return Collections.emptySet();
+ }
+
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/L1TextHandlingTest.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/L1TextHandlingTest.java
new file mode 100644
index 0000000..feda08b
--- /dev/null
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/internal/dom/L1TextHandlingTest.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (c) 2012 Florian Thienel and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ * Florian Thienel - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.vex.core.internal.dom;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.eclipse.core.runtime.AssertionFailedException;
+import org.eclipse.core.runtime.QualifiedName;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @author Florian Thienel
+ */
+public class L1TextHandlingTest {
+
+ private Document document;
+ private Element titleElement;
+
+ @Before
+ public void setUp() throws Exception {
+ document = new Document(new Element("root"));
+ titleElement = new Element("title");
+ document.insertElement(1, titleElement);
+ document.setValidator(new DummyValidator() {
+ @Override
+ public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> nodes, final boolean partial) {
+ return "title".equals(element.getLocalName());
+ }
+
+ @Override
+ public boolean isValidSequence(final QualifiedName element, final List<QualifiedName> sequence1, final List<QualifiedName> sequence2, final List<QualifiedName> sequence3,
+ final boolean partial) {
+ return "title".equals(element.getLocalName());
+ }
+ });
+ }
+
+ @Test
+ public void shouldIndicateValidTextInsertionPoints() throws Exception {
+ assertFalse("the root element may not contain textual contant", document.canInsertText(titleElement.getStartOffset() - 1));
+ assertFalse("the start offset is the last insertion point of the root element before the title element", document.canInsertText(titleElement.getStartOffset()));
+ assertTrue("append new content before the end offset of the title element", document.canInsertText(titleElement.getEndOffset()));
+ assertFalse("the root element may not contain textual contant", document.canInsertText(titleElement.getEndOffset() + 1));
+ }
+
+ @Test
+ public void insertTextAtValidInsertionPoint() throws Exception {
+ document.insertText(titleElement.getEndOffset(), "Hello World");
+ assertEquals("Hello World", titleElement.getText());
+ }
+
+ @Test(expected = AssertionFailedException.class)
+ public void cannotInsertTextBeforeDocumentStart() throws Exception {
+ document.insertText(-1, "Hello World");
+ }
+
+ @Test(expected = AssertionFailedException.class)
+ public void cannotInsertTextAtDocumentStartOffset() throws Exception {
+ document.insertText(0, "Hello World");
+ }
+
+ @Test(expected = AssertionFailedException.class)
+ public void cannotInsertTextAfterDocumentEndOffset() throws Exception {
+ document.insertText(document.getEndOffset() + 1, "Hello World");
+ }
+
+ @Test(expected = DocumentValidationException.class)
+ public void cannotInsertTextAtInvalidInsertionPoint() throws Exception {
+ // titleElement.startOffset is the last insertion point in root before title; root may not contain text according to the validator
+ document.insertText(titleElement.getStartOffset(), "Hello World");
+ }
+
+ @Test
+ public void shouldConvertControlCharactersToSpaces() throws Exception {
+ document.insertText(titleElement.getEndOffset(), "\0\u001F\n");
+ assertEquals("control characters except \\n are converted to spaces", " \n", titleElement.getText());
+ }
+}
diff --git a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/tests/VEXCoreTestSuite.java b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/tests/VEXCoreTestSuite.java
index 39f94a5..2df93dd 100644
--- a/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/tests/VEXCoreTestSuite.java
+++ b/org.eclipse.vex.core.tests/src/org/eclipse/vex/core/tests/VEXCoreTestSuite.java
@@ -29,6 +29,7 @@
import org.eclipse.vex.core.internal.dom.DocumentTest;
import org.eclipse.vex.core.internal.dom.DocumentWriterTest;
import org.eclipse.vex.core.internal.dom.GapContentTest;
+import org.eclipse.vex.core.internal.dom.L1TextHandlingTest;
import org.eclipse.vex.core.internal.dom.NamespaceStackTest;
import org.eclipse.vex.core.internal.dom.NamespaceTest;
import org.eclipse.vex.core.internal.dom.ParentTest;
@@ -62,6 +63,7 @@
addTest(new JUnit4TestAdapter(BasicNodeTest.class));
addTest(new JUnit4TestAdapter(ParentTest.class));
addTest(new JUnit4TestAdapter(DocumentTest.class));
+ addTest(new JUnit4TestAdapter(L1TextHandlingTest.class));
addTest(new JUnit4TestAdapter(DocumentFragmentTest.class));
addTestSuite(PropertyTest.class);
addTestSuite(RuleTest.class);
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
index def8e79..3cd7bb7 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Document.java
@@ -160,33 +160,72 @@
return getContent().length();
}
- private boolean canInsertAt(final int offset, final QualifiedName... nodeNames) {
- return canInsertAt(offset, Arrays.asList(nodeNames));
+ public Position createPosition(final int offset) {
+ return getContent().createPosition(offset);
}
- private boolean canInsertAt(final int offset, final List<QualifiedName> nodeNames) {
+ private boolean canInsertAt(final Element insertionParent, final int offset, final QualifiedName... nodeNames) {
+ return canInsertAt(insertionParent, offset, Arrays.asList(nodeNames));
+ }
+
+ private boolean canInsertAt(final Element insertionParent, final int offset, final List<QualifiedName> nodeNames) {
if (validator == null) {
return true;
}
- final Element parent = getElementAt(offset);
- final List<QualifiedName> seq1 = getNodeNames(parent.getChildNodesBefore(offset));
- final List<QualifiedName> seq2 = nodeNames;
- final List<QualifiedName> seq3 = getNodeNames(parent.getChildNodesAfter(offset));
+ if (insertionParent == null) {
+ return false;
+ }
- return validator.isValidSequence(parent.getQualifiedName(), seq1, seq2, seq3, true);
+ final List<QualifiedName> prefix = getNodeNames(insertionParent.getChildNodesBefore(offset));
+ final List<QualifiedName> insertionCandidates = nodeNames;
+ final List<QualifiedName> suffix = getNodeNames(insertionParent.getChildNodesAfter(offset));
+
+ return validator.isValidSequence(insertionParent.getQualifiedName(), prefix, insertionCandidates, suffix, true);
}
- public boolean canInsertFragment(final int offset, final DocumentFragment fragment) {
- return canInsertAt(offset, fragment.getNodeNames());
+ private Element getInsertionParentAt(final int offset) {
+ final Element parent = getElementAt(offset);
+ if (offset == parent.getStartOffset()) {
+ return parent.getParentElement();
+ }
+ return parent;
}
public boolean canInsertText(final int offset) {
- return canInsertAt(offset, Validator.PCDATA);
+ return canInsertAt(getInsertionParentAt(offset), offset, Validator.PCDATA);
}
- public Position createPosition(final int offset) {
- return getContent().createPosition(offset);
+ public void insertText(final int offset, final String text) throws DocumentValidationException {
+ Assert.isTrue(offset > getStartOffset() && offset <= getEndOffset(), MessageFormat.format("Offset must be in [{0}, {1}]", getStartOffset() + 1, getEndOffset()));
+
+ final Element parent = getInsertionParentAt(offset);
+ if (!canInsertAt(parent, offset, Validator.PCDATA)) {
+ throw new DocumentValidationException(MessageFormat.format("Cannot insert text ''{0}'' at offset {1}.", text, offset));
+ }
+
+ final String adjustedText = convertControlCharactersToSpaces(text);
+
+ fireBeforeContentInserted(new DocumentEvent(this, parent, offset, 2, null));
+
+ getContent().insertText(offset, adjustedText);
+
+ final IUndoableEdit edit = undoEnabled ? new InsertTextEdit(offset, adjustedText) : null;
+ fireContentInserted(new DocumentEvent(this, parent, offset, adjustedText.length(), edit));
+ }
+
+ private String convertControlCharactersToSpaces(final String text) {
+ final char[] characters = text.toCharArray();
+ for (int i = 0; i < characters.length; i++) {
+ if (Character.isISOControl(characters[i]) && characters[i] != '\n') {
+ characters[i] = ' ';
+ }
+ }
+ return new String(characters);
+ }
+
+ public boolean canInsertFragment(final int offset, final DocumentFragment fragment) {
+ return canInsertAt(getInsertionParentAt(offset), offset, fragment.getNodeNames());
}
public void delete(final Range range) throws DocumentValidationException {
@@ -369,7 +408,7 @@
throw new IllegalArgumentException("Error inserting element <" + element.getPrefixedName() + ">: offset is " + offset + ", but it must be between 1 and " + (getLength() - 1));
}
- if (!canInsertAt(offset, element.getQualifiedName())) {
+ if (!canInsertAt(getInsertionParentAt(offset), offset, element.getQualifiedName())) {
throw new DocumentValidationException("Cannot insert element " + element.getPrefixedName() + " at offset " + offset);
}
@@ -416,7 +455,7 @@
throw new IllegalArgumentException("Error inserting document fragment");
}
- if (!canInsertAt(offset, fragment.getNodeNames())) {
+ if (!canInsertAt(getInsertionParentAt(offset), offset, fragment.getNodeNames())) {
throw new DocumentValidationException("Cannot insert document fragment");
}
@@ -444,43 +483,6 @@
fireContentInserted(new DocumentEvent(this, parent, offset, fragment.getContent().length(), edit));
}
- public void insertText(final int offset, final String text) throws DocumentValidationException {
- if (offset < 1 || offset >= getLength()) {
- throw new IllegalArgumentException("Offset must be between 1 and n-1");
- }
-
- final boolean isValid;
- if (!getContent().isElementMarker(offset - 1)) {
- isValid = true;
- } else if (!getContent().isElementMarker(offset)) {
- isValid = true;
- } else {
- isValid = canInsertAt(offset, Validator.PCDATA);
- }
-
- if (!isValid) {
- throw new DocumentValidationException("Cannot insert text '" + text + "' at offset " + offset);
- }
-
- // Convert control chars to spaces
- final StringBuffer sb = new StringBuffer(text);
- for (int i = 0; i < sb.length(); i++) {
- if (Character.isISOControl(sb.charAt(i)) && sb.charAt(i) != '\n') {
- sb.setCharAt(i, ' ');
- }
- }
- final String s = sb.toString();
-
- final Element parent = getElementAt(offset);
- fireBeforeContentInserted(new DocumentEvent(this, parent, offset, 2, null));
-
- getContent().insertText(offset, s);
-
- final IUndoableEdit edit = undoEnabled ? new InsertTextEdit(offset, s) : null;
-
- fireContentInserted(new DocumentEvent(this, parent, offset, s.length(), edit));
- }
-
public void fireAttributeChanged(final DocumentEvent e) {
listeners.fireEvent("attributeChanged", e);
}
diff --git a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
index b51ba4f..ae30ac0 100644
--- a/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
+++ b/org.eclipse.vex.core/src/org/eclipse/vex/core/internal/dom/Parent.java
@@ -10,6 +10,7 @@
*******************************************************************************/
package org.eclipse.vex.core.internal.dom;
+import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
@@ -181,7 +182,7 @@
* @return the node at the given offset
*/
public Node getChildNodeAt(final int offset) {
- Assert.isTrue(containsOffset(offset));
+ Assert.isTrue(containsOffset(offset), MessageFormat.format("Offset must be within {0}.", getRange()));
final List<Node> childNodes = getChildNodes();
for (final Node child : childNodes) {
if (child.containsOffset(offset)) {