On 08/18/2014 09:28 PM, Boris Goldowsky wrote:
I’m trying to write a command in Java that structurally changes a DTBook file - promoting a subsection up a level to become a sibling of
its current parent. In DTBook sections are <level1>, <level2>, <level3>, etc, and contain header tags <h1>,
<h2>, <h3>, etc. So my method has to (a) change the element names, etc from <level3> to <level2> and <h3> to
<h2>, and then (b) move the structure.
That much is working, but I find that when I hit ‘undo’ only some of the
changes get undone - the structural change is reverted but tag names are not
set back to what they were, resulting in an invalid document.
My execute method looks like this. I had thought that the beginEdit() and
endEdit() would tell the undo manager the scope of everything that needs to be
reverted. PromotionHandler is defined to do the appropriate element name
changes.
public Object execute(DocumentView doc, String param, int x, int y) {
MarkManager markManager = doc.getMarkManager();
markManager.beginMark();
Document document = doc.getDocument();
document.beginEdit();
// Rewrite element names inside this element to one level higher
Element e = getSelectedElement(doc);
Traversal.traverse(e, new PromotionHandler());
// Move the element to be a sibling of, and just before, its
current parent.
Element parent = e.getParentElement();
parent.removeChild(e);
parent.getParentElement().insertChild(parent, e);
document.endEdit();
doc.describeUndo("Promote level");
markManager.endMark();
return null;
}
Any idea what I’m missing?
Our mistake.
Element.setName
(http://www.xmlmind.com/xmleditor/_distrib/doc/api/com/xmlmind/xml/doc/Element.html#setName(com.xmlmind.xml.name.Name))
is definitely not supported by the UndoManager.
This method has been added recently for use in very specific cases (e.g.
when the DOM of XXE is used *outside* XXE as a general purpose DOM
similar to W3C DOM or JDOM).
Therefore, this method should have been marked: "Not part of the public
documented API. Do not use.".
I'm afraid that this makes your command much harder to write.
I've attached to this email:
src/com/xmlmind/xmleditext/docbook/Promote.java
src/com/xmlmind/xmleditext/docbook/Demote.java
in the hope that this source code helps you.
/*
* Copyright (c) 2006-2013 Pixware SARL. All rights reserved.
*
* Author: Hussein Shafie
*
* This file is part of the XMLmind XML Editor project.
* For conditions of distribution and use, see the accompanying legal.txt file.
*/
package com.xmlmind.xmleditext.docbook;
import com.xmlmind.util.StringList;
import com.xmlmind.xml.name.Namespace;
import com.xmlmind.xml.name.Name;
import com.xmlmind.xml.doc.Node;
import com.xmlmind.xml.doc.Text;
import com.xmlmind.xml.doc.Element;
import com.xmlmind.xml.doc.Document;
import com.xmlmind.xml.doc.Traversal;
import com.xmlmind.xmledit.edit.Mark;
import com.xmlmind.xmledit.edit.NodeMark;
import com.xmlmind.xmledit.edit.TextLocation;
import com.xmlmind.xmledit.edit.MarkManager;
import com.xmlmind.xmledit.edit.ElementEditor;
import com.xmlmind.xmledit.view.DocumentView;
import com.xmlmind.xmledit.cmd.RecordableCommand;
public final class Promote extends RecordableCommand {
private Element selected;
// Scratch variables.
private Element para;
// -----------------------------------------------------------------------
public boolean prepare(DocumentView docView,
String parameter, int x, int y) {
selected = docView.getSelectedElement(/*implicit*/ false);
Element parent;
Element grandParent;
if (selected == null ||
(parent = selected.getParentElement()) == null ||
(grandParent = parent.getParentElement()) == null ||
!parent.isEditable() ||
!grandParent.isEditable() ||
!isSection(parent)) {
selected = null;
return false;
}
boolean prepared = false;
if (isSection(selected)) {
if (canDeleteRange(selected, parent.getLastChild())) {
if (selected.isEditable()) {
prepared = true;
} else {
// It is possible to promote the last read-only
// "section" contained in an editable "section".
//
// This works in this case, and only in this case, because
// selected is not modified at all (e.g. promoteSection
// does nothing).
prepared = ("section".equals(selected.getLocalName()) &&
"section".equals(parent.getLocalName()) &&
selected.getNextSibling() == null);
}
}
} else {
// Cannot promote the title of a section, but can promote starting
// from any para after the title (even if this means adding a para
// to the stripped parent).
ElementEditor elementEditor = docView.getElementEditor();
elementEditor.editElement(parent);
if (para == null) {
para = createPara(selected.getNamespace());
}
prepared =
elementEditor.canReplace(selected, parent.getLastChild(), para);
}
if (!prepared) {
selected = null;
}
return prepared;
}
protected Object doExecute(DocumentView docView,
String parameter, int x, int y) {
Element parentSection = selected.getParentElement();
ElementEditor elementEditor = docView.getElementEditor();
elementEditor.editElement(parentSection);
boolean addPara =
!elementEditor.canDelete(selected, parentSection.getLastChild());
MarkManager markManager = docView.getMarkManager();
markManager.beginMark();
Document doc = docView.getDocument();
doc.beginEdit();
Element newSection;
Node newSectionFirstChild;
if (isSection(selected)) {
newSection = (Element) selected.copy();
Name newName = parentSection.getName();
if (newName != newSection.getName()) {
promoteSection(newSection, newName);
}
// Otherwise nothing to do. Example: "section" contained inside
// "section".
newSectionFirstChild = selected.getNextSibling();
parentSection.removeChild(selected);
} else {
newSection = createSection(parentSection.getName());
newSectionFirstChild = selected;
}
Node node = newSectionFirstChild;
while (node != null) {
Node next = node.getNextSibling();
parentSection.removeChild(node);
newSection.appendChild(node.copy());
node = next;
}
if (addPara) {
parentSection.appendChild(createPara(parentSection.getNamespace()));
}
parentSection.getParent().insertChild(parentSection.getNextSibling(),
newSection);
doc.endEdit();
docView.describeUndo(Msg.msg("promote"));
markManager.remove(Mark.MARK);
markManager.remove(Mark.SELECTED2);
markManager.set(Mark.SELECTED, newSection);
docView.moveDotInto(newSection);
markManager.endMark();
selected = null;
return null;
}
// -----------------------------------------------------------------------
/*package*/ static boolean canDeleteRange(Node firstDeleted,
Node lastDeleted) {
Node previous = firstDeleted.getPreviousSibling();
Node next = lastDeleted.getNextSibling();
return (!Node.sameInclusion(firstDeleted, previous) &&
!Node.sameInclusion(lastDeleted, next));
}
// -----------------------------------------------------------------------
private static class Promoter extends Traversal.HandlerBase {
public Object enterElement(Element element) {
String newLocalName = null;
Name oldName = element.getName();
String oldLocalName = oldName.localPart;
if ("sect2".equals(oldLocalName)) {
newLocalName = "sect1";
} else if ("sect2info".equals(oldLocalName)) {
newLocalName = "sect1info";
} else if ("sect3".equals(oldLocalName)) {
newLocalName = "sect2";
} else if ("sect3info".equals(oldLocalName)) {
newLocalName = "sect2info";
} else if ("sect4".equals(oldLocalName)) {
newLocalName = "sect3";
} else if ("sect4info".equals(oldLocalName)) {
newLocalName = "sect3info";
} else if ("sect5".equals(oldLocalName)) {
newLocalName = "sect4";
} else if ("sect5info".equals(oldLocalName)) {
newLocalName = "sect4info";
}
// Nothing to do for section and sectioninfo
if (newLocalName != null) {
Name newName = Name.get(oldName.namespace, newLocalName);
if (newName != oldName) {
element.setName(newName);
}
}
return null;
}
}
private static Promoter promoter;
private static final void promoteSection(Element section,
Name promotedName) {
if (promoter == null) {
promoter = new Promoter();
}
String newMetaName = null;
String promotedLocalName = promotedName.localPart;
if ("chapter".equals(promotedLocalName)) {
newMetaName = "chapterinfo";
} else if ("appendix".equals(promotedLocalName)) {
newMetaName = "appendixinfo";
}
if (newMetaName != null) {
String oldMetaName = section.getLocalName() + "info";
Node node = section.getFirstChild();
while (node != null) {
if (node.getType() == Node.Type.ELEMENT) {
Element element = (Element) node;
if (element.getLocalName().equals(oldMetaName)) {
element.setName(Name.get(element.getNamespace(),
newMetaName));
break;
}
}
node = node.getNextSibling();
}
}
section.setName(promotedName);
Node child = section.getFirstChild();
while (child != null) {
Node nextChild = child.getNextSibling();
Traversal.traverse(child, promoter);
child = nextChild;
}
}
// -----------------------------------------------------------------------
/*package*/ static final Element createSection(Name sectionName) {
Element section = new Element(sectionName);
Element title = new Element(Name.get(sectionName.namespace,
"title"));
title.appendChild(new Text());
section.appendChild(title);
return section;
}
/*package*/ static final Element createPara(Namespace ns) {
Element para = new Element(Name.get(ns, "para"));
para.appendChild(new Text());
return para;
}
// -----------------------------------------------------------------------
private static final String[] sectionNames = {
"chapter",
"appendix",
"section",
"sect1",
"sect2",
"sect3",
"sect4",
"sect5"
};
/*package*/ static final boolean isSection(Element element) {
String localName = element.getLocalName();
return StringList.contains(sectionNames, localName);
}
}
/*
* Copyright (c) 2006-2013 Pixware SARL. All rights reserved.
*
* Author: Hussein Shafie
*
* This file is part of the XMLmind XML Editor project.
* For conditions of distribution and use, see the accompanying legal.txt file.
*/
package com.xmlmind.xmleditext.docbook;
import com.xmlmind.util.StringList;
import com.xmlmind.xml.name.Name;
import com.xmlmind.xml.doc.Node;
import com.xmlmind.xml.doc.Text;
import com.xmlmind.xml.doc.Element;
import com.xmlmind.xml.doc.Document;
import com.xmlmind.xml.doc.Traversal;
import com.xmlmind.xmledit.edit.Mark;
import com.xmlmind.xmledit.edit.MarkManager;
import com.xmlmind.xmledit.edit.ElementEditor;
import com.xmlmind.xmledit.view.DocumentView;
import com.xmlmind.xmledit.cmd.RecordableCommand;
public final class Demote extends RecordableCommand {
private Element selected;
// Scratch variables.
private Element para;
// -----------------------------------------------------------------------
public boolean prepare(DocumentView docView,
String parameter, int x, int y) {
selected = docView.getSelectedElement(/*implicit*/ false);
boolean isSection = false;
Element parent;
if (selected == null ||
(parent = selected.getParentElement()) == null ||
!parent.isEditable() ||
!((isSection = Promote.isSection(selected)) ||
Promote.isSection(parent))) {
selected = null;
return false;
}
boolean prepared = false;
if (isSection) {
if (Promote.canDeleteRange(selected, selected) &&
Traversal.traverse(selected, sect5Finder) == null) {
if (selected.isEditable()) {
prepared = true;
} else {
// It is possible to demote a non-editable "section"
// because in such case, demoteSection does nothing at all.
prepared = "section".equals(selected.getLocalName());
}
}
} else {
// Cannot demote the title of a section, but can demote starting
// from any para after the title.
ElementEditor elementEditor = docView.getElementEditor();
elementEditor.editElement(parent);
Node selected2 = extendSelection(selected);
if (selected2 == null) {
selected2 = selected;
}
if (para == null) {
para = Promote.createPara(selected.getNamespace());
}
prepared = elementEditor.canReplace(selected, selected2, para);
}
if (!prepared) {
selected = null;
}
return prepared;
}
protected Object doExecute(DocumentView docView,
String parameter, int x, int y) {
MarkManager markManager = docView.getMarkManager();
markManager.beginMark();
Document doc = docView.getDocument();
doc.beginEdit();
Element newSection;
if (Promote.isSection(selected)) {
Element precedingSection = precedingSection(selected);
if (precedingSection != null && precedingSection.isEditable()) {
selected.getParent().removeChild(selected);
newSection = (Element) selected.copy();
demoteSection(newSection, isSectI(precedingSection));
precedingSection.appendChild(newSection);
} else {
newSection = Promote.createSection(selected.getName());
Element subSection = (Element) selected.copy();
demoteSection(subSection, isSectI(selected));
newSection.appendChild(subSection);
selected.getParent().replaceChild(selected, newSection);
}
} else {
Element parentSection = selected.getParentElement();
newSection = Promote.createSection(parentSection.getName());
newSection.appendChild(selected.copy());
Node selected2 = extendSelection(selected);
if (selected2 != null) {
Node node = selected.getNextSibling();
while (node != null) {
Node next = node.getNextSibling();
parentSection.removeChild(node);
newSection.appendChild(node.copy());
if (node == selected2) {
break;
}
node = next;
}
}
// This is just used to change the name of the new section.
demoteSection(newSection, isSectI(parentSection));
parentSection.replaceChild(selected, newSection);
}
doc.endEdit();
docView.describeUndo(Msg.msg("demote"));
markManager.remove(Mark.MARK);
markManager.remove(Mark.SELECTED2);
markManager.set(Mark.SELECTED, newSection);
docView.moveDotInto(newSection);
markManager.endMark();
selected = null;
return null;
}
// -----------------------------------------------------------------------
private static class Sect5Finder extends Traversal.HandlerBase {
public Object enterElement(Element element) {
if ("sect5".equals(element.getLocalName())) {
return element;
}
return null;
}
}
private static final Sect5Finder sect5Finder = new Sect5Finder();
// -----------------------------------------------------------------------
private static final Element precedingSection(Element section) {
Node node = section.getPreviousSibling();
while (node != null) {
if (node.getType() == Node.Type.ELEMENT) {
if (section.getName() == ((Element) node).getName()) {
return (Element) node;
}
// Done.
break;
}
node = node.getPreviousSibling();
}
return null;
}
// -----------------------------------------------------------------------
private static final String[] sectINames = {
"sect1",
"sect2",
"sect3",
"sect4",
"sect5"
};
private static class SectIFinder extends Traversal.HandlerBase {
public Object enterElement(Element element) {
String localName = element.getLocalName();
if (StringList.contains(sectINames, localName)) {
return element;
}
return null;
}
}
private static SectIFinder sectIFinder;
private static final boolean isSectI(Element element) {
if (sectIFinder == null) {
sectIFinder = new SectIFinder();
}
Object result = Traversal.traverse(element, sectIFinder);
return (result != null);
}
// -----------------------------------------------------------------------
private static class Demoter extends Traversal.HandlerBase {
public boolean sectI;
public Object enterElement(Element element) {
String newLocalName = null;
Name oldName = element.getName();
String oldLocalName = oldName.localPart;
if ("chapter".equals(oldLocalName)) {
newLocalName = sectI? "sect1" : "section";
} else if ("chapterinfo".equals(oldLocalName)) {
newLocalName = sectI? "sect1info" : "sectioninfo";
} else if ("appendix".equals(oldLocalName)) {
newLocalName = sectI? "sect1" : "section";
} else if ("appendixinfo".equals(oldLocalName)) {
newLocalName = sectI? "sect1info" : "sectioninfo";
} else if ("sect1".equals(oldLocalName)) {
newLocalName = "sect2";
} else if ("sect1info".equals(oldLocalName)) {
newLocalName = "sect2info";
} else if ("sect2".equals(oldLocalName)) {
newLocalName = "sect3";
} else if ("sect2info".equals(oldLocalName)) {
newLocalName = "sect3info";
} else if ("sect3".equals(oldLocalName)) {
newLocalName = "sect4";
} else if ("sect3info".equals(oldLocalName)) {
newLocalName = "sect4info";
} else if ("sect4".equals(oldLocalName)) {
newLocalName = "sect5";
} else if ("sect4info".equals(oldLocalName)) {
newLocalName = "sect5info";
}
// Nothing to do for section and sectioninfo
if (newLocalName != null) {
Name newName = Name.get(oldName.namespace, newLocalName);
if (newName != oldName) {
element.setName(newName);
}
}
return null;
}
}
private static Demoter demoter;
private static final void demoteSection(Element section, boolean sectI) {
if (demoter == null) {
demoter = new Demoter();
}
demoter.sectI = sectI;
Traversal.traverse(section, demoter);
}
// -----------------------------------------------------------------------
private static final Node extendSelection(Node selected) {
Node prevNode = null;
Node node = selected.getNextSibling();
while (node != null) {
if (node.getType() == Node.Type.ELEMENT &&
Promote.isSection((Element) node)) {
break;
}
prevNode = node;
node = node.getNextSibling();
}
return prevNode;
}
}
--
XMLmind XML Editor Support List
[email protected]
http://www.xmlmind.com/mailman/listinfo/xmleditor-support