/* * Copyright 2013-2016, Plutext Pty Ltd. * * This file is part of docx4j. docx4j is licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package org.docx4j.toc; import java.io.StringWriter; import java.math.BigInteger; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.bind.JAXBElement; import org.apache.commons.lang3.StringUtils; import org.docx4j.TextUtils; import org.docx4j.XmlUtils; import org.docx4j.model.PropertyResolver; import org.docx4j.model.listnumbering.Emulator.ResultTriple; import org.docx4j.model.properties.paragraph.Indent; import org.docx4j.model.structure.PageDimensions; import org.docx4j.model.structure.SectionWrapper; import org.docx4j.model.styles.StyleUtil; import org.docx4j.wml.BooleanDefaultTrue; import org.docx4j.wml.CTTabStop; import org.docx4j.wml.FldChar; import org.docx4j.wml.ObjectFactory; import org.docx4j.wml.P; import org.docx4j.wml.P.Hyperlink; import org.docx4j.wml.PPr; import org.docx4j.wml.PPrBase; import org.docx4j.wml.PPrBase.Ind; import org.docx4j.wml.ParaRPr; import org.docx4j.wml.R; import org.docx4j.wml.RPr; import org.docx4j.wml.RStyle; import org.docx4j.wml.STFldCharType; import org.docx4j.wml.STTabJc; import org.docx4j.wml.STTabTlc; import org.docx4j.wml.Tabs; import org.docx4j.wml.Text; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class TocEntry { private static Logger log = LoggerFactory.getLogger(TocEntry.class); private TocEntry() {} public TocEntry(PropertyResolver propertyResolver, PageDimensions pageDimensions, STTabTlc leader) { this.propertyResolver = propertyResolver; this.writableWidthTwips = pageDimensions.getWritableWidthTwips(); this.leader = leader; } private PropertyResolver propertyResolver; private static final String PRESERVE = "preserve"; private static final String PAGEREF_MASK = "PAGEREF %s \\h"; private static final String HYPERLINK = "Hyperlink"; //private static final int DOTS_POSITION = 9345; /** * right tab pos= pg width – (gutter + left + right margin) best to put that explicitly in the ToC p, rather than the toc styles (which we just override) Word doesn’t update the right tab pos (where its in a ToC style) in response to a change in margin! @since 3.2.0 */ private int writableWidthTwips; private STTabTlc leader; // private String entryValue; private List<R> entryValues = new ArrayList<R>(); private String anchorValue; private String number = ""; private int entryLevel = -1; private boolean hyperlink = false; private boolean pageNumber = true; /** * If this ToC entry has a paragraph number, we'll need to * allow for that in our tabs definition */ private boolean isNumbered = false; private P entryP; // be careful; this is only set very late in the process private Text pageNumberText; ObjectFactory wmlObjectFactory = new ObjectFactory(); // public void setEntryValue(String entryValue) { // this.entryValue = entryValue; // } public List<R> getEntryValue() { return entryValues; } public void setAnchorValue(String anchorValue) { this.anchorValue = anchorValue; } public String getAnchorValue() { return anchorValue; } public void makeHyperlink(boolean hyperlink) { this.hyperlink = hyperlink; } public void addPageNumber(boolean pageNumber) { this.pageNumber = pageNumber; } public boolean isPageNumber() { return pageNumber; } public int getEntryLevel() { return entryLevel; } public void setEntryLevel(int entryLevel) { this.entryLevel = entryLevel; } public P getEntryParagraph(TocStyles tocStyles){ if(entryP == null){ entryP = generateTocEntry(tocStyles); } return entryP; } public Text getEntryPageNumberText(){ if(pageNumberText == null && entryP != null){ // Get the last empty w:t List<Object> texts = TocHelper.getAllElementsFromObject(entryP, Text.class); Text t; for(Object o: texts){ t = (Text)o; if(t.getValue().isEmpty()){ pageNumberText = t; } } } return pageNumberText; } private P generateTocEntry(TocStyles tocStyles){ // Create object for p P p3 = wmlObjectFactory.createP(); p3.setPPr(generateTocEntryPPr(tocStyles)); if(hyperlink){ p3.getContent().add(generateTocEntryHyperlink()); } else { p3.getContent().addAll(generateTocEntryContent()); } return p3; } private Map<String, Ind> styleIndent = new HashMap<String, Ind>(); private Ind getInd(String styleId) { if (styleId==null) return null; Ind ind = styleIndent.get(styleId); if (ind == null) { PPr ppr = propertyResolver.getEffectivePPr(styleId); if (ppr==null) { return null; } else { ind = ppr.getInd(); styleIndent.put(styleId, ind); } } return ind; } private PPr generateTocEntryPPr(TocStyles tocStyles){ String styleId = tocStyles.getStyleIdForName(String.format(TocStyles.TOC_STYLE_MASK, entryLevel+1)); // Create object for pPr PPr ppr3 = wmlObjectFactory.createPPr(); // Create object for rPr ParaRPr pararpr2 = wmlObjectFactory.createParaRPr(); ppr3.setRPr(pararpr2); // Create object for noProof BooleanDefaultTrue booleandefaulttrue20 = wmlObjectFactory.createBooleanDefaultTrue(); pararpr2.setNoProof(booleandefaulttrue20); // Create object for tabs Tabs tabs2 = wmlObjectFactory.createTabs(); ppr3.setTabs(tabs2); if (isNumbered) { // add a simple tab definition, as our first tab CTTabStop tabstop = wmlObjectFactory.createCTTabStop(); tabs2.getTab().add(tabstop); tabstop.setVal(org.docx4j.wml.STTabJc.LEFT); // Work out how much indentation is required for our tab stop Ind ind = getInd( styleId); // take width of number into account - it could be something like 'Appendix 1'! int numWidth = 110 * numChars; // crude, similar to XsltFOFunctions // .. ok for TNR 12 // KNOWN ISSUE: numbering ticking over from 9 to 10, or i to ii to iii // will increase the width of the field. Need LNE to tell us a numChars value which takes that into account int paddedWidth = 330 + numWidth; if (ind!=null && ind.getLeft()!=null) { tabstop.setPos( BigInteger.valueOf( ind.getLeft().intValue() + paddedWidth) ); } else { tabstop.setPos( BigInteger.valueOf( paddedWidth) ); } } // Create object for tab CTTabStop tabstop2 = wmlObjectFactory.createCTTabStop(); tabs2.getTab().add(tabstop2); tabstop2.setVal(STTabJc.RIGHT); tabstop2.setPos(BigInteger.valueOf(writableWidthTwips) ); tabstop2.setLeader(leader); // Create object for pStyle if (styleId==null) { log.warn("No style found for " + String.format(TocStyles.TOC_STYLE_MASK, entryLevel+1) ); } else { PPrBase.PStyle pprbasepstyle3 = wmlObjectFactory.createPPrBasePStyle(); ppr3.setPStyle(pprbasepstyle3); pprbasepstyle3.setVal(styleId); } return ppr3; } private JAXBElement<Hyperlink> generateTocEntryHyperlink(){ // Create object for hyperlink (wrapped in JAXBElement) Hyperlink phyperlink2 = wmlObjectFactory.createPHyperlink(); JAXBElement<Hyperlink> phyperlinkWrapped2 = wmlObjectFactory.createPHyperlink(phyperlink2); phyperlink2.setAnchor(anchorValue); phyperlink2.getContent().addAll(generateTocEntryContent()); return phyperlinkWrapped2; } private List<R> generateTocEntryContent(){ // List<R> rList = new ArrayList<R>(); // // // Create object for r // R r13 = wmlObjectFactory.createR(); // rList.add(r13); // // Create object for rPr // RPr rpr11 = wmlObjectFactory.createRPr(); // r13.setRPr(rpr11); // if(hyperlink){ // // Create object for rStyle // RStyle rstyle2 = wmlObjectFactory.createRStyle(); // rpr11.setRStyle(rstyle2); // rstyle2.setVal(HYPERLINK); // } // // Create object for noProof // BooleanDefaultTrue booleandefaulttrue21 = wmlObjectFactory.createBooleanDefaultTrue(); // rpr11.setNoProof(booleandefaulttrue21); // // // Create object for t (wrapped in JAXBElement) // Text text6 = wmlObjectFactory.createText(); // JAXBElement<Text> textWrapped6 = wmlObjectFactory.createRT(text6); // r13.getContent().add(textWrapped6); // text6.setValue(entryValue); List<R> rList = entryValues; generateTocEntryPageNumber(rList); return rList; } private void generateTocEntryPageNumber(List<R> rList){ // Create object for r R r14 = wmlObjectFactory.createR(); rList.add(r14); // Create object for rPr RPr rpr12 = wmlObjectFactory.createRPr(); r14.setRPr(rpr12); // Create object for noProof BooleanDefaultTrue booleandefaulttrue22 = wmlObjectFactory.createBooleanDefaultTrue(); rpr12.setNoProof(booleandefaulttrue22); // Create object for webHidden BooleanDefaultTrue booleandefaulttrue23 = wmlObjectFactory.createBooleanDefaultTrue(); rpr12.setWebHidden(booleandefaulttrue23); if(pageNumber){ // Create object for tab (wrapped in JAXBElement) R.Tab rtab2 = wmlObjectFactory.createRTab(); JAXBElement<R.Tab> rtabWrapped2 = wmlObjectFactory.createRTab(rtab2); r14.getContent().add(rtabWrapped2); } // Create object for r R r15 = wmlObjectFactory.createR(); rList.add(r15); // Create object for rPr RPr rpr13 = wmlObjectFactory.createRPr(); r15.setRPr(rpr13); // Create object for noProof BooleanDefaultTrue booleandefaulttrue24 = wmlObjectFactory.createBooleanDefaultTrue(); rpr13.setNoProof(booleandefaulttrue24); // Create object for webHidden BooleanDefaultTrue booleandefaulttrue25 = wmlObjectFactory.createBooleanDefaultTrue(); rpr13.setWebHidden(booleandefaulttrue25); // Create object for fldChar (wrapped in JAXBElement) FldChar fldchar6 = wmlObjectFactory.createFldChar(); JAXBElement<FldChar> fldcharWrapped6 = wmlObjectFactory.createRFldChar(fldchar6); r15.getContent().add( fldcharWrapped6); fldchar6.setFldCharType(STFldCharType.BEGIN); // Create object for r R r16 = wmlObjectFactory.createR(); rList.add(r16); // Create object for rPr RPr rpr14 = wmlObjectFactory.createRPr(); r16.setRPr(rpr14); // Create object for noProof BooleanDefaultTrue booleandefaulttrue26 = wmlObjectFactory.createBooleanDefaultTrue(); rpr14.setNoProof(booleandefaulttrue26); // Create object for webHidden BooleanDefaultTrue booleandefaulttrue27 = wmlObjectFactory.createBooleanDefaultTrue(); rpr14.setWebHidden(booleandefaulttrue27); // Create object for instrText (wrapped in JAXBElement) Text text7 = wmlObjectFactory.createText(); JAXBElement<Text> textWrapped7 = wmlObjectFactory.createRInstrText(text7); r16.getContent().add(textWrapped7); text7.setValue(String.format(PAGEREF_MASK, anchorValue)); text7.setSpace(PRESERVE); // Create object for r R r17 = wmlObjectFactory.createR(); rList.add(r17); // Create object for rPr RPr rpr15 = wmlObjectFactory.createRPr(); r17.setRPr(rpr15); // Create object for noProof BooleanDefaultTrue booleandefaulttrue28 = wmlObjectFactory.createBooleanDefaultTrue(); rpr15.setNoProof(booleandefaulttrue28); // Create object for webHidden BooleanDefaultTrue booleandefaulttrue29 = wmlObjectFactory.createBooleanDefaultTrue(); rpr15.setWebHidden(booleandefaulttrue29); // Create object for r R r18 = wmlObjectFactory.createR(); rList.add(r18); // Create object for rPr RPr rpr16 = wmlObjectFactory.createRPr(); r18.setRPr(rpr16); // Create object for noProof BooleanDefaultTrue booleandefaulttrue30 = wmlObjectFactory.createBooleanDefaultTrue(); rpr16.setNoProof(booleandefaulttrue30); // Create object for webHidden BooleanDefaultTrue booleandefaulttrue31 = wmlObjectFactory.createBooleanDefaultTrue(); rpr16.setWebHidden(booleandefaulttrue31); // Create object for fldChar (wrapped in JAXBElement) FldChar fldchar7 = wmlObjectFactory.createFldChar(); JAXBElement<FldChar> fldcharWrapped7 = wmlObjectFactory.createRFldChar(fldchar7); r18.getContent().add(fldcharWrapped7); fldchar7.setFldCharType(STFldCharType.SEPARATE); // Create object for r R r19 = wmlObjectFactory.createR(); rList.add(r19); // Create object for rPr RPr rpr17 = wmlObjectFactory.createRPr(); r19.setRPr(rpr17); // Create object for noProof BooleanDefaultTrue booleandefaulttrue32 = wmlObjectFactory.createBooleanDefaultTrue(); rpr17.setNoProof(booleandefaulttrue32); // Create object for webHidden BooleanDefaultTrue booleandefaulttrue33 = wmlObjectFactory.createBooleanDefaultTrue(); rpr17.setWebHidden(booleandefaulttrue33); // Create object for t (wrapped in JAXBElement) Text text8 = wmlObjectFactory.createText(); JAXBElement<Text> textWrapped8 = wmlObjectFactory.createRT(text8); r19.getContent().add(textWrapped8); text8.setValue(number); // Create object for r R r20 = wmlObjectFactory.createR(); rList.add(r20); // Create object for rPr RPr rpr18 = wmlObjectFactory.createRPr(); r20.setRPr(rpr18); // Create object for noProof BooleanDefaultTrue booleandefaulttrue34 = wmlObjectFactory.createBooleanDefaultTrue(); rpr18.setNoProof(booleandefaulttrue34); // Create object for webHidden BooleanDefaultTrue booleandefaulttrue35 = wmlObjectFactory.createBooleanDefaultTrue(); rpr18.setWebHidden(booleandefaulttrue35); // Create object for fldChar (wrapped in JAXBElement) FldChar fldchar8 = wmlObjectFactory.createFldChar(); JAXBElement<FldChar> fldcharWrapped8 = wmlObjectFactory.createRFldChar(fldchar8); r20.getContent().add(fldcharWrapped8); fldchar8.setFldCharType(STFldCharType.END); } // public void setEntryValue(P sourceP) { // // StringWriter sw = new StringWriter(); // try { // TextUtils.extractText(sourceP, sw); // } catch (Exception e) { // log.error(e.getMessage(), e); // } // String entryValue = sw.toString(); // this.setEntryValue(entryValue); // } // since 3.1.0.5 public void setEntryValue(P sourceP) { /* Certain Run formatting on entries is re-used, including: • font face • italic • text highlight • hidden (honoured) • small caps but not • font size • font color • underline */ // Step 1: create a clone of the P P clonedP = (P)XmlUtils.deepCopy(sourceP); // Step 2: make a List<R>, comprising the w:r/w:t contents, // with styles resolved List<Object> runsFound = TocHelper.getAllElementsFromObject(clonedP, R.class); R lastRun = null; for( Object o : runsFound) { R r = (R)o; List<Object> textsFound = TocHelper.getAllElementsFromObject(r, Text.class); if (textsFound.size()>0) { R newR = new R(); if (r.getRPr()==null) { newR.setRPr(wmlObjectFactory.createRPr()); } else { // Resolve the formatting newR.setRPr( getEffectiveRPr(r.getRPr())); // newR.setRPr( // r.getRPr()); // Step 3: strip/filter unwanted run formatting nullify(newR.getRPr()); if (newR.getRPr()==null) { newR.setRPr(wmlObjectFactory.createRPr()); } } // apply hyperlink if appropriate if (hyperlink) { RStyle rstyle = wmlObjectFactory.createRStyle(); newR.getRPr().setRStyle(rstyle); rstyle.setVal(HYPERLINK); } newR.getContent().addAll(textsFound); entryValues.add(newR); lastRun = newR; } } // drop any trailing space off last run (Word does this) if (lastRun!=null) { int size = lastRun.getContent().size(); if (size>0) { Text lastText = (Text)lastRun.getContent().get(size-1); String val = lastText.getValue(); if (val!=null) { lastText.setValue(StringUtils.stripEnd(val, null)); } } } // drop leading space off the first run R firstRun = null; if (runsFound.size()>0) { firstRun = (R)runsFound.get(0); List<Object> textsFound = TocHelper.getAllElementsFromObject(firstRun, Text.class); int size = textsFound.size(); if (size>0) { Text firstText = (Text)textsFound.get(0); String val = firstText.getValue(); if (val!=null) { firstText.setValue(StringUtils.stripStart(val, null)); } } } } private RPr getEffectiveRPr(RPr expressRPr) { RPr resolvedRPr = null; if (expressRPr != null && expressRPr.getRStyle() != null ) { String runStyleId = expressRPr.getRStyle().getVal(); resolvedRPr = propertyResolver.getEffectiveRPr(runStyleId); // remove the style, so it is not set by apply below expressRPr.setRStyle(null); } return StyleUtil.apply(expressRPr, resolvedRPr); } private void nullify(RPr destination) { if (destination==null) return; // The following ARE used/preserved in the ToC // setRFonts(null); // setB(null); // setBCs(null); // setICs(null); // setI(null); // setVanish(null); // setCaps(null); // setSmallCaps(null); // setHighlight(null); // The following are definitely dropped destination.setRStyle(null); destination.setSz(null); destination.setSzCs(null); destination.setColor(null); destination.setU(null); // The following are unknown, so assume dropped destination.setLang(null); destination.setStrike(null); destination.setDstrike(null); destination.setOutline(null); destination.setShadow(null); destination.setEmboss(null); destination.setImprint(null); destination.setSnapToGrid(null); destination.setSpacing(null); destination.setW(null); destination.setKern(null); destination.setPosition(null); destination.setEffect(null); destination.setBdr(null); destination.setShd(null); destination.setVertAlign(null); destination.setRtl(null); destination.setCs(null); destination.setEm(null); destination.setSpecVanish(null); destination.setOMath(null); } /** * Number this entry, if necessary * @param numberTriple */ public void numberEntry(ResultTriple numberTriple) { if (numberTriple!=null && numberTriple.getNumString()!=null) { isNumbered = true; //signal that we need to define the tab setting // it depends on the width of the number if (numberTriple.getBullet()!=null ) { numChars=1; } else if (numberTriple.getNumString()==null) { numChars=0; } else { numChars = numberTriple.getNumString().length(); } R r = wmlObjectFactory.createR(); RPr rpr = wmlObjectFactory.createRPr(); r.setRPr(rpr); if(hyperlink){ // Create object for rStyle RStyle rstyle2 = wmlObjectFactory.createRStyle(); rpr.setRStyle(rstyle2); rstyle2.setVal(HYPERLINK); } // Create object for t (wrapped in JAXBElement) Text t = wmlObjectFactory.createText(); JAXBElement<Text> textWrapped6 = wmlObjectFactory.createRT(t); r.getContent().add(textWrapped6); t.setSpace("preserve"); this.entryValues.add(0, r); /* Add a tab, but in a new run (since the tab isn't to be underlined), * unless w:lvl has <w:suff w:val="space"/> or <w:suff w:val="nothing"/> */ if (numberTriple.getLvl().getSuff()==null) { t.setValue(numberTriple.getNumString() ); this.entryValues.add(1, tabAfterPNumber()); } else if ( numberTriple.getLvl().getSuff().getVal().equals("space")) { t.setValue(numberTriple.getNumString() + " "); } else if ( numberTriple.getLvl().getSuff().getVal().equals("nothing")) { t.setValue(numberTriple.getNumString()); } else { // for anything else, ignore, and use a tab (mimicking Word 2010) t.setValue(numberTriple.getNumString() ); this.entryValues.add(1, tabAfterPNumber()); } } } /** Add a tab, but in a new run (since the tab isn't to be underlined), * unless w:lvl has <w:suff w:val="space"/> or <w:suff w:val="nothing"/> */ private R tabAfterPNumber() { R r = wmlObjectFactory.createR(); R.Tab rtab = wmlObjectFactory.createRTab(); JAXBElement<R.Tab> rtabWrapped = wmlObjectFactory.createRTab(rtab); r.getContent().add(rtabWrapped); return r; } int numChars=1; }