View Javadoc
1   /*===========================================================================
2     Copyright (C) 2016-2017 by the Okapi Framework contributors
3   -----------------------------------------------------------------------------
4     Licensed under the Apache License, Version 2.0 (the "License");
5     you may not use this file except in compliance with the License.
6     You may obtain a copy of the License at
7   
8     http://www.apache.org/licenses/LICENSE-2.0
9   
10    Unless required by applicable law or agreed to in writing, software
11    distributed under the License is distributed on an "AS IS" BASIS,
12    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13    See the License for the specific language governing permissions and
14    limitations under the License.
15  ===========================================================================*/
16  
17  package net.sf.okapi.filters.openxml;
18  
19  import static net.sf.okapi.filters.openxml.XMLEventHelpers.LOCAL_BREAK;
20  import static net.sf.okapi.filters.openxml.XMLEventHelpers.LOCAL_TAB;
21  import static net.sf.okapi.filters.openxml.XMLEventHelpers.createQName;
22  
23  import java.util.ArrayDeque;
24  import java.util.ArrayList;
25  import java.util.Deque;
26  import java.util.List;
27  import java.util.Map;
28  
29  import javax.xml.namespace.QName;
30  import javax.xml.stream.XMLEventFactory;
31  import javax.xml.stream.events.Attribute;
32  import javax.xml.stream.events.XMLEvent;
33  
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import net.sf.okapi.common.LocaleId;
38  import net.sf.okapi.common.resource.Code;
39  import net.sf.okapi.common.resource.Segment;
40  import net.sf.okapi.common.resource.TextContainer;
41  import net.sf.okapi.common.resource.TextFragment;
42  import net.sf.okapi.common.resource.TextPart;
43  
44  /*
45   * @version okapi v1.47.0
46   * SV: Modified write(TextContainer tc) to include text of TextParts
47   * SV: Removed the unused private locale field
48   * SV: added combined XML events in writeRunStart()
49   */
50  
51  class BlockTextUnitWriter implements TextUnitWriter {
52  	private static final String EMPTY = "";
53  	private final Logger LOGGER = LoggerFactory.getLogger(BlockTextUnitWriter.class);
54  
55  	private final ConditionalParameters parameters;
56  	private final XMLEventFactory eventFactory;
57  	private final QName runName;
58  	private final QName textName;
59  	private final RunPropertiesPairWithDetectedRunFonts baseRunPropertiesPairWithDetectedRunFonts;
60  	private final List<XMLEvents> hiddenCodes;
61  	private final Map<Integer, XMLEvents> visibleCodes;
62  	private final AdditiveCollection<XMLEvent> xmlEvents;
63  	private final RunPropertiesClarification runPropertiesClarification;
64  	private final Deque<RunPropertiesPairWithDetectedRunFonts> currentRunPropertiesPairWithDetectedRunFonts;
65  	private StringBuilder textContent;
66  	private boolean runIsOpen;
67  
68  	BlockTextUnitWriter(
69  		final ConditionalParameters parameters,
70  		final XMLEventFactory eventFactory,
71  		final LocaleId locale,
72  		final QName runName,
73  		final QName textName,
74  		final RunPropertiesPairWithDetectedRunFonts baseRunPropertiesPairWithDetectedRunFonts,
75  		final List<XMLEvents> hiddenCodes,
76  		final Map<Integer, XMLEvents> visibleCodes,
77  		final AdditiveCollection<XMLEvent> xmlEvents,
78  		final RunPropertiesClarification runPropertiesClarification
79  	) {
80  		this(
81  			parameters,
82  			eventFactory,
83  			locale,
84  			runName,
85  			textName,
86  			baseRunPropertiesPairWithDetectedRunFonts,
87  			hiddenCodes,
88  			visibleCodes,
89  			xmlEvents,
90  			runPropertiesClarification,
91  			new ArrayDeque<>()
92  		);
93  	}
94  
95  	BlockTextUnitWriter(
96  		final ConditionalParameters parameters,
97  		final XMLEventFactory eventFactory,
98  		final LocaleId locale,
99  		final QName runName,
100 		final QName textName,
101 		final RunPropertiesPairWithDetectedRunFonts baseRunPropertiesPairWithDetectedRunFonts,
102 		final List<XMLEvents> hiddenCodes,
103 		final Map<Integer, XMLEvents> visibleCodes,
104 		final AdditiveCollection<XMLEvent> xmlEvents,
105 		final RunPropertiesClarification runPropertiesClarification,
106 		final Deque<RunPropertiesPairWithDetectedRunFonts> currentRunPropertiesPairWithDetectedRunFonts
107 	) {
108 		this.parameters = parameters;
109 		this.eventFactory = eventFactory;
110 		this.runName = runName;
111 		this.textName = textName;
112 		this.baseRunPropertiesPairWithDetectedRunFonts = baseRunPropertiesPairWithDetectedRunFonts;
113 		this.hiddenCodes = hiddenCodes;
114 		this.visibleCodes = visibleCodes;
115 		this.xmlEvents = xmlEvents;
116 		this.runPropertiesClarification = runPropertiesClarification;
117 		this.currentRunPropertiesPairWithDetectedRunFonts = currentRunPropertiesPairWithDetectedRunFonts;
118 	}
119 
120 	public void write(TextContainer tc) {
121     this.textContent = new StringBuilder();
122     boolean firstSegmentWritten = false;
123 
124     for (TextPart textPart : tc) {
125       if (textPart.isSegment()) {
126         Segment segment = (Segment) textPart;
127 
128         if (!firstSegmentWritten) {
129           writeFirstSegment(tc.getFirstSegment());
130           firstSegmentWritten = true;
131           continue;
132         }
133 
134         writeSegment(segment);
135 
136       } else {
137         // textContent.append(textPart.toString());
138         
139         // TextPart can contain codes
140         writeSegment(new Segment("text_part", textPart.getContent()));
141       }
142     }
143 
144     flushText(true);
145   }
146 
147 	private void writeFirstSegment(final Segment segment) {
148 		for (final XMLEvents events : this.hiddenCodes) {
149 			this.xmlEvents.addAll(events.getEvents());
150 		}
151 		writeSegment(segment);
152 	}
153 
154 	private void writeSegment(Segment segment) {
155 		try {
156 			TextFragment content = segment.getContent();
157 			String codedText = content.getCodedText();
158 			List<Code> codes = content.getCodes();
159 			for (int i = 0; i < codedText.length(); i++) {
160 				char c = codedText.charAt(i);
161 				if (TextFragment.isMarker(c)) {
162 					int codeIndex = TextFragment.toIndex(codedText.charAt(++i));
163 					writeCode(codes.get(codeIndex));
164 				}
165 				else {
166 					writeChar(c);
167 				}
168 			}
169 		}
170 		catch (Exception e) {
171 			LOGGER.error("Threw {} writing segment id {} '{}'", e.getClass().getSimpleName(),
172 						 segment.getId(), segment.toString());
173 			throw e;
174 		}
175 	}
176 
177 	private void writeChar(char c) {
178 		textContent.append(c);
179 	}
180 
181 	private void writeCode(Code code) {
182 		// is the Code really an escaped Okapi Marker?
183         if (code.isMarkerMasking()) {
184 			String data = code.getData();
185 			for (int i = 0; i < data.length(); i++) {
186 				char c = data.charAt(i);
187 				writeChar(c);
188 			}
189 			return;
190 		}
191 
192 		// Cases:
193 		// - Open
194 		//   - Terminate current run
195 		//   - Do something content-dependent:   
196 		//	    - If it's RunProperties, update run properties
197 		//	    - If it's a RunContainer, write opening tag
198 		// - Closed
199 		//   - Terminate current run
200 		//   - Handling this is actually optional in many cases
201 		//	 - If it's a RunContainer, write the closing tag
202 		// - Isolated
203 		//   - Terminate current run
204 		//   - Write out the corresponding markup (for Run, Run.RunMarkup, Block.BlockMarkup
205 		int id = code.getId();
206 		XMLEvents codeEvents = visibleCodes.get(id);
207 		switch (code.getTagType()) {
208 			case OPENING:
209 				flushText(true);
210 				if (codeEvents instanceof RunPropertiesPairWithDetectedRunFonts) {
211 					this.currentRunPropertiesPairWithDetectedRunFonts.push((RunPropertiesPairWithDetectedRunFonts) codeEvents);
212 				}
213 				else if (codeEvents instanceof RunContainer) {
214 					RunContainer rc = (RunContainer)codeEvents;
215 					this.xmlEvents.addAll(rc.startMarkupEvents());
216 					this.currentRunPropertiesPairWithDetectedRunFonts.push(rc.defaultRunPropertiesPairWithDetectedRunFonts());
217 				}
218 				else {
219 					throw new IllegalStateException("Unexpected code contents for opening code '" +
220 													code.toString() + "':" + codeEvents );
221 				}
222 				break;
223 			case PLACEHOLDER:
224 				// If this is RunMarkup (markup contained within a run), we should
225 				// keep the current run open.  Otherwise, close it.
226 				boolean isRunMarkup = (codeEvents instanceof Run.Markup);
227 				if (isRunMarkup) {
228 					flushText(false);
229 					flushRunStart();
230 				} else {
231 					flushText(true);
232 				}
233 				this.xmlEvents.addAll(codeEvents.getEvents());
234 				break;
235 			case CLOSING:
236 				flushText(true);
237 				if (codeEvents instanceof RunPropertiesPairWithDetectedRunFonts) {
238 					// XXX What if it's not on the top of the stack?  It's probably a corrupt target.
239 					this.currentRunPropertiesPairWithDetectedRunFonts.pop();
240 				}
241 				else if (codeEvents instanceof RunContainer) {
242 					RunContainer rc = (RunContainer) codeEvents;
243 					this.xmlEvents.addAll(rc.endMarkupEvents());
244 					this.currentRunPropertiesPairWithDetectedRunFonts.pop(); // Pop RunContainer properties
245 				}
246 				else {
247 					throw new IllegalStateException("Unexpected code contents for closing code '" +
248 													code.toString() + "':" + codeEvents );
249 				}
250 
251 				break;
252 		}
253 	}
254 
255 	private void flushRunStart() {
256 		if (!runIsOpen) {
257 			writeRunStart();
258 			runIsOpen = true;
259 		}
260 	}
261 
262 	private void flushRunEnd() {
263 		if (runIsOpen) {
264 			writeRunEnd();
265 			runIsOpen = false;
266 		}
267 	}
268 
269 	private void flushText(boolean terminateRun) {
270 		final RunPropertiesPairWithDetectedRunFonts rppwdrf = runPropertiesPairWithDetectedRunFonts();
271 		if (textContent.length() > 0) {
272 			final String text = textContent.toString();
273 			this.runPropertiesClarification.prepareContextWith(
274 				rppwdrf.combined(),
275 				rppwdrf.detectedRunFonts(),
276 				text
277 			);
278 			flushRunStart();
279 			writeRunText(text);
280 			textContent = new StringBuilder();
281 		} else {
282 			this.runPropertiesClarification.prepareContextWith(
283 				rppwdrf.combined(),
284 				rppwdrf.detectedRunFonts(),
285 				EMPTY
286 			);
287 		}
288 		if (terminateRun) {
289 			flushRunEnd();
290 		}
291 	}
292 
293 	private RunPropertiesPairWithDetectedRunFonts runPropertiesPairWithDetectedRunFonts() {
294 		return this.currentRunPropertiesPairWithDetectedRunFonts.isEmpty()
295 			? this.baseRunPropertiesPairWithDetectedRunFonts
296 			: this.currentRunPropertiesPairWithDetectedRunFonts.peek();
297 	}
298 
299 	private void writeRunStart() {
300     final RunPropertiesPairWithDetectedRunFonts rpp = runPropertiesPairWithDetectedRunFonts();
301     xmlEvents.add(eventFactory.createStartElement(runName, null, null));
302     
303     runPropertiesClarification.performFor(rpp.direct());
304     xmlEvents.addAll(rpp.direct().getEvents());
305     
306     runPropertiesClarification.performFor(rpp.combined());
307     xmlEvents.addAll(rpp.combined().getEvents());
308   }
309 
310 	// Would be better to have a separate hierarcy for the MS Word BlockTextUnitWrite.java and for the Excel
311 	// BlockTextUnitWrite.java but...
312 	private void writeRunText(String text) {
313 		// MS Excel doesn't support the line breaks inside text run
314 		// We should save a content as is
315 		// Current implementation of ms excel text run has <t> without prefix
316 		// We are using this fact to catch an excel text runs
317 		if (textName.getPrefix().isEmpty()) {
318 			writeTextIfNeeded(new StringBuilder(text));
319 			return;
320 		}
321 
322 		// MS Word text runs can contain the line breaks
323 		// The text run of ms word looks like <w:t>
324 		// The prefix "w" says us that is ms word text tun
325 		StringBuilder sb = new StringBuilder();
326 		for (char c : text.toCharArray()) {
327 			if (c == parameters.getLineSeparatorReplacement() && parameters.getAddLineSeparatorCharacter()) {
328 				writeTextIfNeeded(sb);
329 				sb.setLength(0);
330 				writeLineBreak();
331 			} else if (c == '\t' && parameters.getAddTabAsCharacter()
332 					&& Namespace.PREFIX_W.equals(this.textName.getPrefix())) {
333 				writeTextIfNeeded(sb);
334 				sb.setLength(0);
335 				writeTab();
336 			} else {
337 				sb.append(c);
338 			}
339 		}
340 		writeTextIfNeeded(sb);
341 	}
342 
343 	private void writeTextIfNeeded(StringBuilder buffer) {
344 		if (buffer.length() > 0) {
345 			flushRunStart();
346 			writeText(buffer.toString());
347 		}
348 	}
349 
350 	private void writeTab() {
351 		QName br = createQName(LOCAL_TAB, textName);
352 		xmlEvents.add(eventFactory.createStartElement(br, null, null));
353 		xmlEvents.add(eventFactory.createEndElement(br, null));
354 	}
355 
356 	private void writeLineBreak() {
357 		// Word and PowerPoint seems to always start a new run before the break element. Although
358 		// this is not enforced by specification we behave like Word. This prevents some strange
359 		// behaviour and broken documents.
360 		if (Namespace.PREFIX_A.equals(this.textName.getPrefix())) {
361 			// The a:br element has to be on the same level as a:r.
362 			flushRunEnd();
363 			writeRunStart();
364 			writeText("");
365 			writeRunEnd();
366 		} else {
367 			writeRunEnd();
368 			writeRunStart();
369 		}
370 		QName br = createQName(LOCAL_BREAK, textName);
371 		xmlEvents.add(eventFactory.createStartElement(br, null, null));
372 		if (Namespace.PREFIX_A.equals(this.textName.getPrefix())) {
373 			final RunPropertiesPairWithDetectedRunFonts rpp = runPropertiesPairWithDetectedRunFonts();
374 			runPropertiesClarification.performFor(rpp.direct());
375 			xmlEvents.addAll(rpp.direct().getEvents());
376 		}
377 		xmlEvents.add(eventFactory.createEndElement(br, null));
378 	}
379 
380 	private void writeText(String text) {
381 		boolean needsPreserveSpace = needsXmlSpacePreserve(text);
382 		ArrayList<Attribute> attrs = new ArrayList<>();
383 		// DrawingML <a:t> does not use the xml:space="preserve" attribute
384 		if (needsPreserveSpace && !Namespaces.DrawingML.containsName(textName)) {
385 			attrs.add(eventFactory.createAttribute("xml", Namespaces.XML.getURI(), "space", "preserve"));
386 		}
387 		xmlEvents.add(eventFactory.createStartElement(textName, attrs.iterator(), null));
388 		xmlEvents.add(eventFactory.createCharacters(text));
389 		xmlEvents.add(eventFactory.createEndElement(textName, null));
390 	}
391 
392 	private void writeRunEnd() {
393 		xmlEvents.add(eventFactory.createEndElement(runName, null));
394 	}
395 
396 	/**
397 	 * Returns true if the given text contains a space, tab or no-break space. In that case you
398 	 * have to add {@code xml:space="preserve"} to the {@code &lt;w:t&gt;} element.
399 	 *
400 	 * @param text text
401 	 * @return true if the given text contains a space, tab or no-break space
402 	 */
403 	static boolean needsXmlSpacePreserve(String text) {
404 		for (char c : text.toCharArray()) {
405 			// This catches things like ideographic space (U+3000).  NBSP
406 			// isn't flagged as whitespace in unicode, so we have to special-case it.
407 			if (Character.isWhitespace(c) || c == '\u00A0') return true;
408 		}
409 		return false;
410 	}
411 }