/************************************************************************ * * FieldConverter.java * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License version 2.1, as published by the Free Software Foundation. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA * * Copyright: 2002-2008 by Henrik Just * * All Rights Reserved. * * Version 1.0 (2008-11-23) * */ package writer2latex.latex; //import java.io.UnsupportedEncodingException; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; import org.w3c.dom.Element; import org.w3c.dom.Node; import writer2latex.latex.util.Context; import writer2latex.latex.util.HeadingMap; import writer2latex.office.OfficeReader; import writer2latex.office.XMLString; import writer2latex.util.CSVList; import writer2latex.util.ExportNameCollection; import writer2latex.util.Misc; import writer2latex.util.SimpleInputBuffer; /** * This class handles text fields and links in the document. * Packages: lastpage, hyperref, titleref, oooref (all optional) * TODO: Need proper treatment of "caption" and "text" for sequence * references not to figures and tables (should be fairly rare, though) */ public class FieldConverter extends ConverterHelper { // Links & references private ExportNameCollection targets = new ExportNameCollection(true); private ExportNameCollection refnames = new ExportNameCollection(true); private ExportNameCollection bookmarknames = new ExportNameCollection(true); private ExportNameCollection seqnames = new ExportNameCollection(true); private ExportNameCollection seqrefnames = new ExportNameCollection(true); // sequence declarations (maps name->text:sequence-decl element) private Hashtable<String, Node> seqDecl = new Hashtable<String, Node>(); // first usage of sequence (maps name->text:sequence element) private Hashtable<String, Element> seqFirst = new Hashtable<String, Element>(); private Vector<Element> postponedReferenceMarks = new Vector<Element>(); private Vector<Element> postponedBookmarks = new Vector<Element>(); private boolean bUseHyperref = false; private boolean bUsesPageCount = false; private boolean bUsesTitleref = false; private boolean bUsesOooref = false; public FieldConverter(OfficeReader ofr, LaTeXConfig config, ConverterPalette palette) { super(ofr,config,palette); // hyperref.sty is not compatible with titleref.sty and oooref.sty: bUseHyperref = config.useHyperref() && !config.useTitleref() && !config.useOooref(); } /** <p>Append declarations needed by the <code>FieldConverter</code> to * the preamble.</p> * @param pack the <code>LaTeXDocumentPortion</code> to which * declarations of packages should be added (<code>\\usepackage</code>). * @param decl the <code>LaTeXDocumentPortion</code> to which * other declarations should be added. */ public void appendDeclarations(LaTeXDocumentPortion pack, LaTeXDocumentPortion decl) { // use lastpage.sty if (bUsesPageCount) { pack.append("\\usepackage{lastpage}").nl(); } // use titleref.sty if (bUsesTitleref) { pack.append("\\usepackage{titleref}").nl(); } // use oooref.sty if (bUsesOooref) { pack.append("\\usepackage["); HeadingMap hm = config.getHeadingMap(); CSVList opt = new CSVList(","); for (int i=0; i<=hm.getMaxLevel(); i++) { opt.addValue(hm.getName(i)); } pack.append(opt.toString()).append("]{oooref}").nl(); } // use hyperref.sty if (bUseHyperref){ pack.append("\\usepackage{hyperref}").nl(); pack.append("\\hypersetup{"); if (config.getBackend()==LaTeXConfig.PDFTEX) pack.append("pdftex, "); else if (config.getBackend()==LaTeXConfig.DVIPS) pack.append("dvips, "); //else pack.append("hypertex"); pack.append("colorlinks=true, linkcolor=blue, citecolor=blue, filecolor=blue, urlcolor=blue"); if (config.getBackend()==LaTeXConfig.PDFTEX) { pack.append(createPdfMeta("pdftitle",palette.getMetaData().getTitle())); if (config.metadata()) { pack.append(createPdfMeta("pdfauthor",palette.getMetaData().getCreator())) .append(createPdfMeta("pdfsubject",palette.getMetaData().getSubject())) .append(createPdfMeta("pdfkeywords",palette.getMetaData().getKeywords())); } } pack.append("}").nl(); } // Export sequence declarations // The number format is fetched from the first occurence of the // sequence in the text, while the outline level and the separation // character are fetched from the declaration Enumeration<String> names = seqFirst.keys(); while (names.hasMoreElements()) { // Get first text:sequence element String sName = names.nextElement(); Element first = seqFirst.get(sName); // Collect data String sNumFormat = Misc.getAttribute(first,XMLString.STYLE_NUM_FORMAT); if (sNumFormat==null) { sNumFormat="1"; } int nLevel = 0; String sSepChar = "."; if (seqDecl.containsKey(sName)) { Element sdecl = (Element) seqDecl.get(sName); nLevel = Misc.getPosInteger(sdecl.getAttribute(XMLString.TEXT_DISPLAY_OUTLINE_LEVEL),0); if (sdecl.hasAttribute(XMLString.TEXT_SEPARATION_CHARACTER)) { sSepChar = palette.getI18n().convert( sdecl.getAttribute(XMLString.TEXT_SEPARATION_CHARACTER), false,palette.getMainContext().getLang()); } } // Create counter decl.append("\\newcounter{") .append(seqnames.getExportName(sName)) .append("}"); String sPrefix = ""; if (nLevel>0) { HeadingMap hm = config.getHeadingMap(); int nUsedLevel = nLevel<=hm.getMaxLevel() ? nLevel : hm.getMaxLevel(); if (nUsedLevel>0) { decl.append("[").append(hm.getName(nUsedLevel)).append("]"); sPrefix = "\\the"+hm.getName(nUsedLevel)+sSepChar; } } decl.nl() .append("\\renewcommand\\the") .append(seqnames.getExportName(sName)) .append("{").append(sPrefix) .append(ListStyleConverter.numFormat(sNumFormat)) .append("{").append(seqnames.getExportName(sName)) .append("}}").nl(); } } /** <p>Process sequence declarations</p> * @param node the text:sequence-decls node */ public void handleSequenceDecls(Element node) { Node child = node.getFirstChild(); while (child!=null) { if (Misc.isElement(child,XMLString.TEXT_SEQUENCE_DECL)) { // Don't process the declaration, but store a reference seqDecl.put(((Element)child).getAttribute(XMLString.TEXT_NAME),child); } child = child.getNextSibling(); } } /** <p>Process a sequence field (text:sequence tag)</p> * @param node The element containing the sequence field * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handleSequence(Element node, LaTeXDocumentPortion ldp, Context oc) { String sName = Misc.getAttribute(node,XMLString.TEXT_NAME); String sRefName = Misc.getAttribute(node,XMLString.TEXT_REF_NAME); String sFormula = Misc.getAttribute(node,XMLString.TEXT_FORMULA); if (sFormula==null) { // If there's no formula, we must use the content as formula // The parser below requires a namespace, so we add that.. sFormula = "ooow:"+Misc.getPCDATA(node); } if (sName!=null) { if (ofr.isFigureSequenceName(sName) || ofr.isTableSequenceName(sName)) { // Export \label only, assuming the number is generated by \caption if (sRefName!=null && ofr.hasSequenceRefTo(sRefName)) { ldp.append("\\label{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}"); } } else { // General purpose sequence -> export as counter if (!seqFirst.containsKey(sName)) { // Save first occurence -> used to determine number format seqFirst.put(sName,node); } if (sRefName!=null && ofr.hasSequenceRefTo(sRefName)) { // Export as {\refstepcounter{name}\thename\label{refname}} ldp.append("{").append(changeCounter(sName,sFormula,true)) .append("\\the").append(seqnames.getExportName(sName)) .append("\\label{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}}"); } else { // Export as \stepcounter{name}{\thename} ldp.append(changeCounter(sName,sFormula,false)) .append("{\\the") .append(seqnames.getExportName(sName)) .append("}"); } } } } /** <p>Create label for a sequence field (text:sequence tag)</p> * @param node The element containing the sequence field * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added */ public void handleSequenceLabel(Element node, LaTeXDocumentPortion ldp) { String sRefName = Misc.getAttribute(node,XMLString.TEXT_REF_NAME); if (sRefName!=null && ofr.hasSequenceRefTo(sRefName)) { ldp.append("\\label{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}"); } } // According to the spec for OpenDocument, the formula is application // specific, prefixed with a namespace. OOo uses the namespace ooow, and // we accept the formulas ooow:<number>, ooow:<name>, ooow:<name>+<number> // and ooow:<name>-<number> // Note: In OOo a counter is a 16 bit unsigned integer, whereas a (La)TeX // counter can be negative - thus there will be a slight deviation in the // (rare) case of a negative number private String changeCounter(String sName, String sFormula, boolean bRef) { if (sFormula!=null) { sFormula = sFormula.trim(); if (sFormula.startsWith("ooow:")) { SimpleInputBuffer input = new SimpleInputBuffer(sFormula.substring(5)); if (input.peekChar()>='0' && input.peekChar()<='9') { // Value is <number> String sNumber = input.getInteger(); if (input.atEnd()) { return setCounter(sName, Misc.getPosInteger(sNumber,0), bRef); } } else if (input.peekChar()=='-') { // Value is a negative <number> input.getChar(); if (input.peekChar()>='0' && input.peekChar()<='9') { String sNumber = input.getInteger(); if (input.atEnd()) { return setCounter(sName, -Misc.getPosInteger(sNumber,0), bRef); } } } else { // Value starts with <name> String sToken = input.getIdentifier(); if (sToken.equals(sName)) { input.skipSpaces(); if (input.peekChar()=='+') { // Value is <name>+<number> input.getChar(); input.skipSpaces(); String sNumber = input.getInteger(); if (input.atEnd()) { return addtoCounter(sName, Misc.getPosInteger(sNumber,0), bRef); } } else if (input.peekChar()=='-') { // Value is <name>-<number> input.getChar(); input.skipSpaces(); String sNumber = input.getInteger(); if (input.atEnd()) { return addtoCounter(sName, -Misc.getPosInteger(sNumber,0), bRef); } } else if (input.atEnd()) { // Value is <name> return addtoCounter(sName, 0, bRef); } } } } } // No formula, or a formula we don't understand -> use default behavior return stepCounter(sName, bRef); } private String stepCounter(String sName, boolean bRef) { if (bRef) { return "\\refstepcounter{" + seqnames.getExportName(sName) + "}"; } else { return "\\stepcounter{" + seqnames.getExportName(sName) + "}"; } } private String addtoCounter(String sName, int nValue, boolean bRef) { if (nValue==1) { return stepCounter(sName, bRef); } else if (bRef) { return "\\addtocounter{" + seqnames.getExportName(sName) + "}" + "{" + Integer.toString(nValue-1) + "}" + "\\refstepcounter{" + seqnames.getExportName(sName) + "}"; } else if (nValue!=0) { return "\\addtocounter{" + seqnames.getExportName(sName) + "}" + "{" + Integer.toString(nValue) + "}"; } else { return ""; } } private String setCounter(String sName, int nValue, boolean bRef) { if (bRef) { return "\\setcounter{" + seqnames.getExportName(sName) + "}" + "{" + Integer.toString(nValue-1) + "}" + "\\refstepcounter{" + seqnames.getExportName(sName) + "}"; } else { return "\\setcounter{" + seqnames.getExportName(sName) + "}" + "{" + Integer.toString(nValue) + "}"; } } /** <p>Process a sequence reference (text:sequence-ref tag)</p> * @param node The element containing the sequence reference * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handleSequenceRef(Element node, LaTeXDocumentPortion ldp, Context oc) { String sRefName = Misc.getAttribute(node,XMLString.TEXT_REF_NAME); String sFormat = Misc.getAttribute(node,XMLString.TEXT_REFERENCE_FORMAT); String sName = ofr.getSequenceFromRef(sRefName); if (sRefName!=null) { if (sFormat==null || "page".equals(sFormat)) { ldp.append("\\pageref{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}"); } else if ("value".equals(sFormat)) { ldp.append("\\ref{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}"); } else if ("category-and-value".equals(sFormat)) { // Export as Name~\\ref{refname} if (sName!=null) { if (ofr.isFigureSequenceName(sName)) { ldp.append("\\figurename~"); } else if (ofr.isTableSequenceName(sName)) { ldp.append("\\tablename~"); } else { ldp.append(sName).append("~"); } } ldp.append("\\ref{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}"); } else if ("chapter".equals(sFormat) && config.useOooref()) { ldp.append("\\chapterref{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}"); bUsesOooref = true; } else if ("caption".equals(sFormat) && config.useTitleref() && (ofr.isFigureSequenceName(sName) || ofr.isTableSequenceName(sName))) { ldp.append("\\titleref{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}"); bUsesTitleref = true; } else if ("text".equals(sFormat) && config.useTitleref() && (ofr.isFigureSequenceName(sName) || ofr.isTableSequenceName(sName))) { // This is a combination of "category-and-value" and "caption" // Export as \\figurename~\ref{refname}:~\titleref{refname} if (ofr.isFigureSequenceName(sName)) { ldp.append("\\figurename"); } else if (ofr.isTableSequenceName(sName)) { ldp.append("\\tablename"); } ldp.append("~\\ref{seq:") .append(seqrefnames.getExportName(sRefName)) .append("}:~\\titleref{") .append(seqrefnames.getExportName(sRefName)) .append("}"); bUsesTitleref = true; } else { // use current value palette.getInlineCv().traversePCDATA(node,ldp,oc); } } } /** <p>Process a reference mark (text:reference-mark tag)</p> * @param node The element containing the reference mark * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handleReferenceMark(Element node, LaTeXDocumentPortion ldp, Context oc) { if (!oc.isInSection() && !oc.isInCaption() && !oc.isVerbatim()) { // Note: Always include \label here, even when it's not used String sName = node.getAttribute(XMLString.TEXT_NAME); if (sName!=null) { ldp.append("\\label{ref:"+refnames.getExportName(sName)+"}"); } } else { // Reference marks should not appear within \section or \caption postponedReferenceMarks.add(node); } } /** <p>Process a reference (text:reference-ref tag)</p> * @param node The element containing the reference * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handleReferenceRef(Element node, LaTeXDocumentPortion ldp, Context oc) { String sFormat = node.getAttribute(XMLString.TEXT_REFERENCE_FORMAT); String sName = node.getAttribute(XMLString.TEXT_REF_NAME); if (("page".equals(sFormat) || "".equals(sFormat)) && sName!=null) { ldp.append("\\pageref{ref:"+refnames.getExportName(sName)+"}"); } else if ("chapter".equals(sFormat) && ofr.referenceMarkInHeading(sName)) { // This is safe if the reference mark is contained in a heading ldp.append("\\ref{ref:"+refnames.getExportName(sName)+"}"); } else { // use current value palette.getInlineCv().traversePCDATA(node,ldp,oc); } } /** <p>Process a bookmark (text:bookmark tag)</p> * <p>A bookmark may be the target for either a hyperlink or a reference, * so this will generate a <code>\\hyperref</code> and/or a <code>\\label</code></p> * @param node The element containing the bookmark * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handleBookmark(Element node, LaTeXDocumentPortion ldp, Context oc) { if (!oc.isInSection() && !oc.isInCaption() && !oc.isVerbatim()) { String sName = node.getAttribute(XMLString.TEXT_NAME); if (sName!=null) { // A bookmark may be used as a target for a hyperlink as well as // for a reference. We export whatever is actually used: addTarget(node,"",ldp); if (ofr.hasBookmarkRefTo(sName)) { ldp.append("\\label{bkm:"+bookmarknames.getExportName(sName)+"}"); } } } else { // Bookmarks should not appear within \section or \caption postponedBookmarks.add(node); } } /** <p>Process a bookmark reference (text:bookmark-ref tag).</p> * @param node The element containing the bookmark reference * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handleBookmarkRef(Element node, LaTeXDocumentPortion ldp, Context oc) { String sFormat = node.getAttribute(XMLString.TEXT_REFERENCE_FORMAT); String sName = node.getAttribute(XMLString.TEXT_REF_NAME); if (("page".equals(sFormat) || "".equals(sFormat)) && sName!=null) { ldp.append("\\pageref{bkm:"+bookmarknames.getExportName(sName)+"}"); } else if ("chapter".equals(sFormat) && ofr.bookmarkInHeading(sName)) { // This is safe if the bookmark is contained in a heading ldp.append("\\ref{bkm:"+bookmarknames.getExportName(sName)+"}"); } else { // use current value palette.getInlineCv().traversePCDATA(node,ldp,oc); } } /** <p>Process pending reference marks and bookmarks (which may have been * postponed within sections, captions or verbatim text.</p> * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void flushReferenceMarks(LaTeXDocumentPortion ldp, Context oc) { // We may still be in a context with no reference marks if (!oc.isInSection() && !oc.isInCaption() && !oc.isVerbatim()) { // Type out all postponed reference marks int n = postponedReferenceMarks.size(); for (int i=0; i<n; i++) { handleReferenceMark(postponedReferenceMarks.get(i),ldp,oc); } postponedReferenceMarks.clear(); // Type out all postponed bookmarks n = postponedBookmarks.size(); for (int i=0; i<n; i++) { handleBookmark(postponedBookmarks.get(i),ldp,oc); } postponedBookmarks.clear(); } } /** <p>Process a hyperlink (text:a tag)</p> * @param node The element containing the hyperlink * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handleAnchor(Element node, LaTeXDocumentPortion ldp, Context oc) { String sHref = node.getAttribute(XMLString.XLINK_HREF); if (sHref!=null) { if (sHref.startsWith("#")) { // TODO: hyperlinks to headings (?) and objects if (bUseHyperref) { ldp.append("\\hyperlink{") .append(targets.getExportName(Misc.urlDecode(sHref.substring(1)))) .append("}{"); // ignore text style (let hyperref.sty handle the decoration): palette.getInlineCv().traverseInlineText(node,ldp,oc); ldp.append("}"); } else { // user don't want to include hyperlinks palette.getInlineCv().handleTextSpan(node,ldp,oc); } } else { if (bUseHyperref) { if (ofr.getTextContent(node).trim().equals(sHref)) { // The link text equals the url ldp.append("\\url{") .append(oc.isInFootnote() ? escapeHref(Misc.urlDecode(sHref)) : Misc.urlDecode(sHref)) .append("}"); } else { ldp.append("\\href{") .append(oc.isInFootnote() ? escapeHref(Misc.urlDecode(sHref)) : Misc.urlDecode(sHref)) .append("}{"); // ignore text style (let hyperref.sty handle the decoration): palette.getInlineCv().traverseInlineText(node,ldp,oc); ldp.append("}"); } } else { // user don't want to include hyperlinks palette.getInlineCv().handleTextSpan(node,ldp,oc); } } } else { palette.getInlineCv().handleTextSpan(node,ldp,oc); } } /** <p>Add a <code>\\hypertarget</code></p> * @param node The element containing the name of the target * @param sSuffix A suffix to be added to the target, * e.g. "|table" for a reference to a table. * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added */ public void addTarget(Element node, String sSuffix, LaTeXDocumentPortion ldp) { // TODO: Remove this and use addTarget by name only String sName = node.getAttribute(XMLString.TEXT_NAME); if (sName == null) { sName = node.getAttribute(XMLString.TABLE_NAME); } if (sName == null || !bUseHyperref) { return; } if (!ofr.hasLinkTo(sName+sSuffix)) { return; } ldp.append("\\hypertarget{") .append(targets.getExportName(sName+sSuffix)) .append("}{}"); } /** <p>Add a <code>\\hypertarget</code></p> * @param sName The name of the target * @param sSuffix A suffix to be added to the target, * e.g. "|table" for a reference to a table. * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added */ public void addTarget(String sName, String sSuffix, LaTeXDocumentPortion ldp) { if (sName!=null && bUseHyperref && ofr.hasLinkTo(sName+sSuffix)) { ldp.append("\\hypertarget{") .append(targets.getExportName(sName+sSuffix)) .append("}{}"); } } /** <p>Process a page number field (text:page-number tag)</p> * @param node The element containing the page number field * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handlePageNumber(Element node, LaTeXDocumentPortion ldp, Context oc) { // TODO: Obey attributes! ldp.append("\\thepage{}"); } /** <p>Process a page count field (text:page-count tag)</p> * @param node The element containing the page count field * @param ldp the <code>LaTeXDocumentPortion</code> to which * LaTeX code should be added * @param oc the current context */ public void handlePageCount(Element node, LaTeXDocumentPortion ldp, Context oc) { // TODO: Obey attributes! // Note: Actually LastPage refers to the page number of the last page, not the number of pages if (config.useLastpage()) { bUsesPageCount = true; ldp.append("\\pageref{LastPage}"); } else { ldp.append("?"); } } // Helpers: private String createPdfMeta(String sName, String sValue) { if (sValue==null) { return ""; } // Replace commas with semicolons (the keyval package doesn't like commas): sValue = sValue.replace(',', ';'); // Meta data is assumed to be in the default language: return ", "+sName+"="+palette.getI18n().convert(sValue,false,palette.getMainContext().getLang()); } // For href within footnote, we have to escape the # private String escapeHref(String s) { StringBuffer buf = new StringBuffer(); for (int i=0; i<s.length(); i++) { if (s.charAt(i)=='#') { buf.append("\\#"); } else { buf.append(s.charAt(i)); } } return buf.toString(); } }