package nota.oxygen.epub.headings; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.List; import javax.swing.text.BadLocationException; import nota.oxygen.common.BaseAuthorOperation; import nota.oxygen.common.Utils; import nota.oxygen.epub.EpubUtils; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import ro.sync.ecss.extensions.api.ArgumentDescriptor; import ro.sync.ecss.extensions.api.ArgumentsMap; import ro.sync.ecss.extensions.api.AuthorAccess; import ro.sync.ecss.extensions.api.AuthorDocumentController; import ro.sync.ecss.extensions.api.AuthorOperationException; import ro.sync.ecss.extensions.api.node.AttrValue; import ro.sync.ecss.extensions.api.node.AuthorElement; import ro.sync.ecss.extensions.api.node.AuthorNode; public class UpdateNavigationDocumentsOperation extends BaseAuthorOperation { private AuthorAccess authorAccess; private AuthorAccess opfAccess; private Document ncx; private Document xhtmlNav; private List<NavItem> pageItems; private List<TocItem> topLevelTocItems; private int playOrder; @Override public ArgumentDescriptor[] getArguments() { return new ArgumentDescriptor[]{}; } @Override public String getDescription() { return "Updates the navigation documents of a ePub 3 document (ncx and xhtml nav documents)"; } @Override public void doOperation() throws AuthorOperationException { if (getAuthorAccess() == null) { opfAccess = authorAccess; } else { authorAccess = getAuthorAccess(); URL opfUrl = EpubUtils.getPackageUrl(authorAccess); if (opfUrl == null) { showMessage("Could not find pagkage file for document"); return; } opfAccess = EpubUtils.getAuthorDocument(getAuthorAccess(), opfUrl); if (opfAccess == null) { showMessage("Could not access pagkage file for document"); } } AuthorAccess ncxAccess = EpubUtils.getNCXDocument(opfAccess); ncx = Utils.getDOMDocument(ncxAccess); AuthorAccess xhtmlNavAccess = EpubUtils.getXHTMLNavDocument(opfAccess); xhtmlNav = Utils.getDOMDocument(xhtmlNavAccess); pageItems = new ArrayList<NavItem>(); topLevelTocItems = new ArrayList<TocItem>(); playOrder = 0; for (AuthorAccess textDocAccess : EpubUtils.getSpine(opfAccess, true)) { topLevelTocItems.addAll(getTocItems(textDocAccess, opfAccess)); } foldTocItemsByPartAndChapter(); updateNcx(); updateXhtmlNav(); ncxAccess.getDocumentController().beginCompoundEdit(); try { Utils.replaceRoot(ncx, ncxAccess); xhtmlNavAccess.getDocumentController().beginCompoundEdit(); try { Utils.replaceRoot(xhtmlNav, xhtmlNavAccess); } catch (AuthorOperationException e) { xhtmlNavAccess.getDocumentController().cancelCompoundEdit(); throw e; } } catch (AuthorOperationException e) { ncxAccess.getDocumentController().cancelCompoundEdit(); throw e; } ncxAccess.getDocumentController().endCompoundEdit(); xhtmlNavAccess.getDocumentController().endCompoundEdit(); Utils.bringFocusToDocumentTab(authorAccess); } private Element createSkeletonNcxRootElement() throws AuthorOperationException { Element ncxElement = ncx.createElementNS(EpubUtils.NCX_NS, "ncx"); ncxElement.setAttribute("version", "2005-1"); ncxElement.appendChild(ncx.createElementNS(EpubUtils.NCX_NS, "head")); Element docTitleElement = ncx.createElementNS(EpubUtils.NCX_NS, "docTitle"); for (AuthorNode node : opfAccess.getDocumentController().findNodesByXPath("/package/metadata/dc:title", true, true, true)) { try { docTitleElement.setTextContent(node.getTextContent()); break; } catch (DOMException e) { continue; } catch (BadLocationException e) { continue; } } ncxElement.appendChild(docTitleElement); ncxElement.appendChild(ncx.createElementNS(EpubUtils.NCX_NS, "head")); return ncxElement; } private void updateNcx() throws AuthorOperationException { if (ncx == null) return; Element ncxElement = ncx.getDocumentElement(); if (ncxElement == null) { ncxElement = createSkeletonNcxRootElement(); ncx.appendChild(ncxElement); } Element pageListElement = Utils.getChildElementByNameNS(ncxElement, EpubUtils.NCX_NS, "pageList"); if (pageListElement == null) { pageListElement = ncx.createElementNS(EpubUtils.NCX_NS, "pageList"); pageListElement.appendChild(createNavLabel("List of Pages")); Element navListElement = Utils.getChildElementByNameNS(ncxElement, EpubUtils.NCX_NS, "navList"); if (navListElement == null) { ncxElement.appendChild(pageListElement); } else { ncxElement.insertBefore(pageListElement, navListElement); } } Element navMapElement = Utils.getChildElementByNameNS(ncxElement, EpubUtils.NCX_NS, "navMap"); if (navMapElement == null) { navMapElement = ncx.createElementNS(EpubUtils.NCX_NS, "navMap"); navMapElement.appendChild(createNavLabel("Table of Content")); ncxElement.insertBefore(navMapElement, pageListElement); } //Assertion: ncx is at minimum a skeleton ncx with navMap and pageList for (Element navPoint : Utils.getChildElementsByNameNS(navMapElement, EpubUtils.NCX_NS, "navPoint")) { navMapElement.removeChild(navPoint); } int depth = 0; for (int i = 0; i < topLevelTocItems.size(); i++) { TocItem tocItem = topLevelTocItems.get(i); navMapElement.appendChild(tocItem.getAsNcxNavPoint()); if (tocItem.getDepth() > depth) { depth = tocItem.getDepth(); } } int maxPageNormal = 0; if (pageItems.size() > 0) { for (Element pageTarget : Utils.getChildElementsByNameNS(pageListElement, EpubUtils.NCX_NS, "pageTarget")) { pageListElement.removeChild(pageTarget); } for (int i = 0; i < pageItems.size(); i++) { NavItem pageItem = pageItems.get(i); pageListElement.appendChild(pageItem.getAsNcxPageTarget()); if (pageItem.isOfClass("page-normal") && maxPageNormal < pageItem.getTextAsInteger()) { maxPageNormal = pageItem.getTextAsInteger(); } } } else { ncxElement.removeChild(pageListElement); } Element headElement = Utils.getChildElementByNameNS(ncxElement, EpubUtils.NCX_NS, "head"); if (headElement == null) { headElement = ncx.createElementNS(EpubUtils.NCX_NS, "head"); ncxElement.insertBefore(headElement, ncxElement.getFirstChild()); } Element depthMetaElement = null; Element totalPageCountMetaElement = null; Element maxPageNumberMetaElement = null; for (Element meta : Utils.getChildElementsByNameNS(headElement, EpubUtils.NCX_NS, "meta")) { switch (meta.getAttribute("name")) { case "dtb:depth": depthMetaElement = meta; break; case "dtb:totalPageCount": totalPageCountMetaElement = meta; break; case "dtb:maxPageNumber": maxPageNumberMetaElement = meta; break; } } if (depthMetaElement == null) { depthMetaElement = ncx.createElementNS(EpubUtils.NCX_NS, "meta"); depthMetaElement.setAttribute("name", "dtb:depth"); headElement.appendChild(depthMetaElement); } depthMetaElement.setAttribute("content", String.format("%d", depth)); if (totalPageCountMetaElement == null) { totalPageCountMetaElement = ncx.createElementNS(EpubUtils.NCX_NS, "meta"); totalPageCountMetaElement.setAttribute("name", "dtb:totalPageCount"); headElement.appendChild(totalPageCountMetaElement); } totalPageCountMetaElement.setAttribute("content", String.format("%d", pageItems.size())); if (maxPageNumberMetaElement == null) { maxPageNumberMetaElement = ncx.createElementNS(EpubUtils.NCX_NS, "meta"); maxPageNumberMetaElement.setAttribute("name", "dtb:maxPageNumber"); headElement.appendChild(maxPageNumberMetaElement); } maxPageNumberMetaElement.setAttribute("content", String.format("%d", maxPageNormal)); } private void updateXhtmlNav() { if (xhtmlNav == null) return; Element htmlElement = xhtmlNav.getDocumentElement(); if (htmlElement == null) { htmlElement = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "html"); htmlElement.appendChild(xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "head")); xhtmlNav.appendChild(htmlElement); } if (htmlElement.getAttribute("xmlns:epub")==null) { htmlElement.setAttribute("xmlns:epub", EpubUtils.EPUB_NS); } Element bodyElement = Utils.getChildElementByNameNS(htmlElement, EpubUtils.XHTML_NS, "body"); if (bodyElement==null) { bodyElement = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "body"); bodyElement.setAttributeNS(EpubUtils.EPUB_NS, "epub:type", "frontmatter"); htmlElement.appendChild(bodyElement); } Element tocNav = null; Element pageListNav = null; for (Element nav : Utils.getChildElementsByNameNS(bodyElement, EpubUtils.XHTML_NS, "nav")) { for (String type : nav.getAttributeNS(EpubUtils.EPUB_NS, "type").split("\\s+")) { switch (type) { case "toc": tocNav = nav; break; case "page-list": pageListNav = nav; break; } } } if (tocNav == null) { tocNav = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "nav"); tocNav.setAttributeNS(EpubUtils.EPUB_NS, "epub:type", "toc"); Element h1 = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "h1"); h1.setTextContent("Table of Contents"); tocNav.appendChild(h1); bodyElement.appendChild(tocNav); } for (Element tocOl : Utils.getChildElementsByNameNS(tocNav, EpubUtils.XHTML_NS, "ol")) { tocNav.removeChild(tocOl); } Element tocOl = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "ol"); for (int i = 0; i < topLevelTocItems.size(); i++) { tocOl.appendChild(topLevelTocItems.get(i).getAsXhtmlListItem()); } tocNav.appendChild(tocOl); if (pageListNav == null) { pageListNav = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "nav"); pageListNav.setAttributeNS(EpubUtils.EPUB_NS, "epub:type", "page-list"); Element h1 = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "h1"); h1.setTextContent("List of Pages"); pageListNav.appendChild(h1); bodyElement.appendChild(pageListNav); } for (Element pageListOl : Utils.getChildElementsByNameNS(pageListNav, EpubUtils.XHTML_NS, "ol")) { pageListNav.removeChild(pageListOl); } Element pageListOl = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "ol"); for (int i = 0; i < pageItems.size(); i++) { pageListOl.appendChild(pageItems.get(i).getAsXhtmlListItem()); } pageListNav.appendChild(pageListOl); } private void foldTocItemsByPartAndChapter() { int i = 1; while (i < topLevelTocItems.size()) { if (topLevelTocItems.get(i-1).isOfType("part") && topLevelTocItems.get(i).isOfType("chapter")) { topLevelTocItems.get(i-1).childItems.add(topLevelTocItems.remove(i)); continue; } i++; } } private List<TocItem> getTocItems(AuthorAccess textDocAccess, AuthorAccess opfAccess) throws AuthorOperationException { try { URI textDocUri = URI.create( authorAccess.getUtilAccess().makeRelative( opfAccess.getEditorAccess().getEditorLocation(), textDocAccess.getEditorAccess().getEditorLocation())); // URI textDocUri = URI.create(Utils.relativizeURI( // opfAccess.getEditorAccess().getEditorLocation().toString(), // textDocAccess.getEditorAccess().getEditorLocation().toString())); List<TocItem> res = new ArrayList<TocItem>(); AuthorElement htmlElem = textDocAccess.getDocumentController().getAuthorDocumentNode().getRootElement(); if (htmlElem != null) { AuthorElement bodyElem = null; for (AuthorNode node : htmlElem.getElementsByLocalName("body")) { bodyElem = (AuthorElement)node; break; } if (bodyElem != null) { //REMARK: if body element has a epub:type attribute it will act as a section element, // otherwise the body element is assumed to be a container for section elements if (bodyElem.getAttribute("epub:type") != null) { res.add(getTocItem(bodyElem, textDocAccess.getDocumentController(), textDocUri)); } else { AuthorElement[] sectionElements = bodyElem.getElementsByLocalName("section"); for (int i = 0; i < sectionElements.length; i++) { res.add(getTocItem(sectionElements[i], textDocAccess.getDocumentController(), textDocUri)); } } } } return res; } catch (AuthorOperationException e) { throw e; } catch (Exception e) { throw new AuthorOperationException( String.format( "An unexpected %s occured while getting toc items: %s", e.getClass().getName(), e.getMessage()), e); } } private TocItem getTocItem(AuthorElement sectionElement, AuthorDocumentController textDocCtrl, URI textDocUri) throws AuthorOperationException { playOrder++; TocItem item = new TocItem(); item.epubType = (sectionElement.getAttribute("epub:type")!=null) ? sectionElement.getAttribute("epub:type").getValue() : ""; item.order = playOrder; item.text = "***"; if (sectionElement.getAttribute("id") == null && !"body".equals(sectionElement.getLocalName())) { textDocCtrl.getUniqueAttributesProcessor().assignUniqueIDs(sectionElement.getStartOffset()-1, sectionElement.getEndOffset()+1, true); } item.targetUri = getTargetUri(sectionElement, textDocUri); for (AuthorNode node : textDocCtrl.findNodesByXPath("h1|h2|h3|h4|h5|h6", sectionElement, true, true, true, true)) { AuthorElement hx = (AuthorElement)node; try { item.text = hx.getTextContent(); } catch (BadLocationException e) { continue; } break; } for (AuthorNode child : sectionElement.getContentNodes()) { if (child instanceof AuthorElement) { AuthorElement elem = (AuthorElement)child; if (elem.getLocalName().equals("section")) { item.childItems.add(getTocItem(elem, textDocCtrl, textDocUri)); } else { addPageItems(elem, textDocUri); } } } return item; } private URI getTargetUri(AuthorElement elem, URI textDocUri) { URI res = textDocUri; AttrValue idVal = elem.getAttribute("id"); if (idVal != null) { res = res.resolve(String.format("#%s", idVal.getValue())); } return res; } private void addPageItems(AuthorElement elem, URI textDocUri) { NavItem pageItem = new NavItem(); pageItem.epubType = (elem.getAttribute("epub:type")!=null) ? elem.getAttribute("epub:type").getValue() : ""; if (pageItem.isOfType("pagebreak")) { playOrder++; pageItem.order = playOrder; pageItem.text = (elem.getAttribute("title")!=null) ? elem.getAttribute("title").getValue() : ""; pageItem.classValue = (elem.getAttribute("class")!=null) ? elem.getAttribute("class").getValue() : ""; pageItem.targetUri = getTargetUri(elem, textDocUri); pageItems.add(pageItem); } else { for (AuthorNode node : elem.getContentNodes()) { if (node instanceof AuthorElement) addPageItems((AuthorElement)node, textDocUri); } } } @Override protected void parseArguments(ArgumentsMap args) throws IllegalArgumentException { // Nothing to parse!!! } private Element createNavLabel(String text) { Element navLabelElement = ncx.createElementNS(EpubUtils.NCX_NS, "navLabel"); Element textElement = ncx.createElementNS(EpubUtils.NCX_NS, "text"); textElement.setTextContent(text); navLabelElement.appendChild(textElement); return navLabelElement; } public void setAuthorAccess(AuthorAccess authorAccess) { this.authorAccess = authorAccess; } public class NavItem { public URI targetUri; public String text; public int order; public String epubType; public String classValue; public boolean isOfType(String type) { if (epubType == null) return false; for (String t : epubType.split("\\s+")) { if (t.equals(type)) return true; } return false; } public boolean isOfClass(String cls) { if (classValue == null) return false; for (String t : classValue.split("\\s+")) { if (t.equals(cls)) return true; } return false; } public int getTextAsInteger() { try { int res = Integer.parseInt(text); if (res > 0) { return res; } } catch (NumberFormatException e) { //Do nothing, just return 0 at the end } return 0; } protected Element getContent() { Element content = ncx.createElementNS(EpubUtils.NCX_NS, "content"); content.setAttribute("src", targetUri.toString()); return content; } public Element getAsNcxPageTarget() { Element pageTarget = ncx.createElementNS(EpubUtils.NCX_NS, "pageTarget"); pageTarget.setAttribute("id", String.format("pageTarget-%d", order)); pageTarget.setAttribute("playOrder", String.format("%d", order)); pageTarget.setAttribute("type", isOfClass("page-normal") ? "normal" : "special"); pageTarget.appendChild(createNavLabel(text)); pageTarget.appendChild(getContent()); return pageTarget; } public Element getAsXhtmlListItem() { Element li = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "li"); Element a = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "a"); a.setAttribute("href", targetUri.toString()); a.setTextContent(text); li.appendChild(a); return li; } } public class TocItem extends NavItem { public List<TocItem> childItems = new ArrayList<TocItem>(); public int getDepth() { int depth = 1; for (int i = 0; i < childItems.size(); i++) { if (depth < childItems.get(i).getDepth()+1) { depth = childItems.get(i).getDepth()+1; } } return depth; } public Element getAsNcxNavPoint() { Element navPoint = ncx.createElementNS(EpubUtils.NCX_NS, "navPoint"); navPoint.setAttribute("id", String.format("navPoint-%d", order)); navPoint.setAttribute("playOrder", String.format("%d", order)); navPoint.appendChild(createNavLabel(text)); navPoint.appendChild(getContent()); for (TocItem child : childItems) { navPoint.appendChild(child.getAsNcxNavPoint()); } return navPoint; } @Override public Element getAsXhtmlListItem() { Element li = super.getAsXhtmlListItem(); if (childItems.size() > 0) { Element ol = xhtmlNav.createElementNS(EpubUtils.XHTML_NS, "ol"); for (int i = 0; i < childItems.size(); i++) { ol.appendChild(childItems.get(i).getAsXhtmlListItem()); } li.appendChild(ol); } return li; } } }