Java tutorial
/************************************************************************ * * 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-2015 by Henrik Just * * All Rights Reserved. * * Version 1.6 (2015-06-23) * */ package writer2latex.latex; //import java.io.UnsupportedEncodingException; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; import java.util.regex.Pattern; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.w3c.dom.Element; import org.w3c.dom.Node; import writer2latex.latex.util.Context; import writer2latex.latex.util.HeadingMap; import writer2latex.office.ListStyle; import writer2latex.office.OfficeReader; import writer2latex.office.XMLString; 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 (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 { // Identify Zotero items private static final String ZOTERO_ITEM = "ZOTERO_ITEM"; // Identify JabRef items private static final String JABREF_ITEM = "JR_cite"; // 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 bConvertZotero = false; private boolean bConvertJabRef = false; private boolean bIncludeOriginalCitations = false; private boolean bUseNatbib = false; public FieldConverter(OfficeReader ofr, LaTeXConfig config, ConverterPalette palette) { super(ofr, config, palette); // hyperref.sty is not compatible with titleref.sty: bUseHyperref = config.useHyperref() && !config.useTitleref(); bConvertZotero = config.useBibtex() && config.zoteroBibtexFiles().length() > 0; bConvertJabRef = config.useBibtex() && config.jabrefBibtexFiles().length() > 0; bIncludeOriginalCitations = config.includeOriginalCitations(); bUseNatbib = config.useBibtex() && config.useNatbib(); } /** <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 natbib if (config.useBibtex() && config.useNatbib()) { pack.append("\\usepackage"); if (config.getNatbibOptions().length() > 0) { pack.append("[").append(config.getNatbibOptions()).append("]"); } pack.append("{natbib}").nl(); } // use lastpage.sty if (bUsesPageCount) { pack.append("\\usepackage{lastpage}").nl(); } // use titleref.sty if (bUsesTitleref) { pack.append("\\usepackage{titleref}").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(ListConverter.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 ("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); } } } // Try to handle this reference name as a Zotero reference, return true on success private boolean handleZoteroReferenceName(String sName, LaTeXDocumentPortion ldp, Context oc) { // First parse the reference name: // A Zotero reference name has the form ZOTERO_ITEM <json object> <identifier> with a single space separating the items // The identifier is a unique identifier for the reference and is not used here if (sName.startsWith(ZOTERO_ITEM)) { int nObjectStart = sName.indexOf('{'); int nObjectEnd = sName.lastIndexOf('}'); if (nObjectStart > -1 && nObjectEnd > -1 && nObjectStart < nObjectEnd) { String sJsonObject = sName.substring(nObjectStart, nObjectEnd + 1); JSONObject jo = null; try { jo = new JSONObject(sJsonObject); } catch (JSONException e) { return false; } // Successfully parsed the reference, now generate the code // (we don't expect any errors and ignore them, if they happen anyway) // Sort key (purpose? currently ignored) /*boolean bSort = true; try { bSort = jo.getBoolean("sort"); } catch (JSONException e) { }*/ JSONArray citationItemsArray = null; try { // The value is an array of objects, one for each source in this citation citationItemsArray = jo.getJSONArray("citationItems"); } catch (JSONException e) { } if (citationItemsArray != null) { int nCitationCount = citationItemsArray.length(); if (bUseNatbib) { if (nCitationCount > 1) { // For multiple citations, use \citetext, otherwise we cannot add individual prefixes and suffixes // TODO: If no prefixes or suffixes exist, it's safe to combine the citations ldp.append("\\citetext{"); } for (int nIndex = 0; nIndex < nCitationCount; nIndex++) { JSONObject citationItems = null; try { // Each citation is represented as an object citationItems = citationItemsArray.getJSONObject(nIndex); } catch (JSONException e) { } if (citationItems != null) { if (nIndex > 0) { ldp.append("; "); // Separate multiple citations in this reference } // Citation items String sURI = ""; boolean bSuppressAuthor = false; String sPrefix = ""; String sSuffix = ""; String sLocator = ""; String sLocatorType = ""; try { // The URI seems to be an array with a single string value(?) sURI = citationItems.getJSONArray("uri").getString(0); } catch (JSONException e) { } try { // SuppressAuthor is a boolean value bSuppressAuthor = citationItems.getBoolean("suppressAuthor"); } catch (JSONException e) { } try { // Prefix is a string value sPrefix = citationItems.getString("prefix"); } catch (JSONException e) { } try { // Suffix is a string value sSuffix = citationItems.getString("suffix"); } catch (JSONException e) { } try { // Locator is a string value, e.g. a page number sLocator = citationItems.getString("locator"); } catch (JSONException e) { } try { // LocatorType is a string value, e.g. book, verse, page (missing locatorType means page) sLocatorType = citationItems.getString("locatorType"); } catch (JSONException e) { } // Adjust locator type (empty locator type means "page") // TODO: Handle other locator types (localize and abbreviate): Currently the internal name (e.g. book) is used. if (sLocator.length() > 0 && sLocatorType.length() == 0) { // A locator of the form <number><other characters><number> is interpreted as several pages if (Pattern.compile("[0-9]+[^0-9]+[0-9]+").matcher(sLocator).find()) { sLocatorType = "pp."; } else { sLocatorType = "p."; } } // Insert command. TODO: Evaluate this if (nCitationCount > 1) { // Use commands without parentheses if (bSuppressAuthor) { ldp.append("\\citeyear"); } else { ldp.append("\\citet"); } } else { if (bSuppressAuthor) { ldp.append("\\citeyearpar"); } else { ldp.append("\\citep"); } } if (sPrefix.length() > 0) { ldp.append("[").append(palette.getI18n().convert(sPrefix, true, oc.getLang())) .append("]"); } if (sPrefix.length() > 0 || sSuffix.length() > 0 || sLocatorType.length() > 0 || sLocator.length() > 0) { // Note that we need to include an empty suffix if there's a prefix! ldp.append("[").append(palette.getI18n().convert(sSuffix, true, oc.getLang())) .append(palette.getI18n().convert(sLocatorType, true, oc.getLang())); if (sLocatorType.length() > 0 && sLocator.length() > 0) { ldp.append("~"); } ldp.append(palette.getI18n().convert(sLocator, true, oc.getLang())).append("]"); } ldp.append("{"); int nSlash = sURI.lastIndexOf('/'); if (nSlash > 0) { ldp.append(sURI.substring(nSlash + 1)); } else { ldp.append(sURI); } ldp.append("}"); } } if (nCitationCount > 1) { // End the \citetext command ldp.append("}"); } } else { // natbib is not available, use simple \cite command ldp.append("\\cite{"); for (int nIndex = 0; nIndex < nCitationCount; nIndex++) { JSONObject citationItems = null; try { // Each citation is represented as an object citationItems = citationItemsArray.getJSONObject(nIndex); } catch (JSONException e) { } if (citationItems != null) { if (nIndex > 0) { ldp.append(","); // Separate multiple citations in this reference } // Citation items String sURI = ""; try { // The URI seems to be an array with a single string value(?) sURI = citationItems.getJSONArray("uri").getString(0); } catch (JSONException e) { } int nSlash = sURI.lastIndexOf('/'); if (nSlash > 0) { ldp.append(sURI.substring(nSlash + 1)); } else { ldp.append(sURI); } } } ldp.append("}"); } oc.setInZoteroJabRefText(true); return true; } } } return false; } // Try to handle this reference name as a JabRef reference, return true on success private boolean handleJabRefReferenceName(String sName, LaTeXDocumentPortion ldp, Context oc) { // First parse the reference name: // A JabRef reference name has the form JR_cite<m>_<n>_<identifiers> where // m is a sequence number to ensure unique citations (may be empty) // n=1 for (Author date) and n=2 for Author (date) citations // identifiers is a comma separated list of BibTeX keys if (sName.startsWith(JABREF_ITEM)) { String sRemains = sName.substring(JABREF_ITEM.length()); int nUnderscore = sRemains.indexOf('_'); if (nUnderscore > -1) { sRemains = sRemains.substring(nUnderscore + 1); if (sRemains.length() > 2) { String sCommand; if (bUseNatbib) { if (sRemains.charAt(0) == '1') { sCommand = "\\citep"; } else { sCommand = "\\citet"; } } else { sCommand = "\\cite"; } ldp.append(sCommand).append("{").append(sRemains.substring(2)).append("}"); } } oc.setInZoteroJabRefText(true); return true; } return false; } private String shortenRefname(String s) { // For Zotero items, use the trailing unique identifier if (s.startsWith(ZOTERO_ITEM)) { int nLast = s.lastIndexOf(' '); if (nLast > 0) { return s.substring(nLast + 1); } } return s; } /** <p>Process a reference mark end (text:reference-mark-end 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 handleReferenceMarkEnd(Element node, LaTeXDocumentPortion ldp, Context oc) { // Nothing to do, except to mark that this ends any Zotero/JabRef citation oc.setInZoteroJabRefText(false); if (bIncludeOriginalCitations) { // Protect space after comment ldp.append("{}"); } } /** <p>Process a reference mark (text:reference-mark or text:reference-mark-start 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()) { String sName = node.getAttribute(XMLString.TEXT_NAME); // Zotero and JabRef (mis)uses reference marks to store citations, so check this first if (sName != null && (!bConvertZotero || !handleZoteroReferenceName(sName, ldp, oc)) && (!bConvertJabRef || !handleJabRefReferenceName(sName, ldp, oc))) { // Plain reference mark // Note: Always include \label here, even when it's not used ldp.append("\\label{ref:" + refnames.getExportName(shortenRefname(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(shortenRefname(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(shortenRefname(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 if (("number".equals(sFormat) || "number-no-superior".equals(sFormat) || "number-all-superior".equals(sFormat)) && (ofr.bookmarkInHeading(sName) || ofr.bookmarkInList(sName))) { ListStyle style = null; int nLevel = 0; String sPrefix = null; String sSuffix = null; // Only convert the prefix and suffix if it is converted at the reference target if (ofr.bookmarkInHeading(sName)) { if (config.formatting() >= LaTeXConfig.CONVERT_MOST) { style = ofr.getOutlineStyle(); } nLevel = ofr.getBookmarkHeadingLevel(sName); } else { if (config.formatting() >= LaTeXConfig.CONVERT_BASIC) { style = ofr.getListStyle(ofr.getBookmarkListStyle(sName)); } nLevel = ofr.getBookmarkListLevel(sName); } if (style != null) { sPrefix = style.getLevelProperty(nLevel, XMLString.STYLE_NUM_PREFIX); sSuffix = style.getLevelProperty(nLevel, XMLString.STYLE_NUM_SUFFIX); } if (sPrefix != null) ldp.append(palette.getI18n().convert(sPrefix, false, oc.getLang())); ldp.append("\\ref{bkm:").append(bookmarknames.getExportName(sName)).append("}"); if (sSuffix != null) ldp.append(palette.getI18n().convert(sSuffix, false, oc.getLang())); } else { // use current value palette.getInlineCv().traversePCDATA(node, ldp, oc); } } /** Do we have any pending reference marks or bookmarks, that may be inserted in this context? * * @param oc the context to verify against * @return true if there are pending marks */ public boolean hasPendingReferenceMarks(Context oc) { return !oc.isInSection() && !oc.isInCaption() && !oc.isVerbatim() && postponedReferenceMarks.size() + postponedBookmarks.size() > 0; } /** <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 (OfficeReader.getTextContent(node).trim().equals(sHref)) { // The link text equals the url ldp.append("\\url{").append(escapeHref(sHref, oc.isInFootnote())).append("}"); } else { ldp.append("\\href{").append(escapeHref(sHref, oc.isInFootnote())).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 the argument to a href, we have to escape or encode certain characters private String escapeHref(String s, boolean bInFootnote) { StringBuilder buf = new StringBuilder(); for (int i = 0; i < s.length(); i++) { if (bInFootnote && s.charAt(i) == '#') { buf.append("\\#"); } else if (bInFootnote && s.charAt(i) == '%') { buf.append("\\%"); } // The following should not occur in an URL (see RFC1738), but just to be sure we encode them else if (s.charAt(i) == '\\') { buf.append("\\%5C"); } else if (s.charAt(i) == '{') { buf.append("\\%7B"); } else if (s.charAt(i) == '}') { buf.append("\\%7D"); } // hyperref.sty deals safely with other characters else { buf.append(s.charAt(i)); } } return buf.toString(); } }