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 net.sf.okapi.common.IdGenerator;
20  import net.sf.okapi.common.XMLEventsReader;
21  import net.sf.okapi.common.resource.TextFragment;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  
25  import javax.xml.namespace.QName;
26  import javax.xml.stream.XMLEventReader;
27  import javax.xml.stream.XMLStreamException;
28  import javax.xml.stream.events.Attribute;
29  import javax.xml.stream.events.Characters;
30  import javax.xml.stream.events.EndElement;
31  import javax.xml.stream.events.StartElement;
32  import javax.xml.stream.events.XMLEvent;
33  import java.util.ArrayList;
34  import java.util.Collection;
35  import java.util.Collections;
36  import java.util.Deque;
37  import java.util.HashSet;
38  import java.util.Iterator;
39  import java.util.LinkedList;
40  import java.util.List;
41  import java.util.Set;
42  
43  import static net.sf.okapi.filters.openxml.XMLEventHelpers.LOCAL_REGULAR_HYPHEN_VALUE;
44  import static net.sf.okapi.filters.openxml.XMLEventHelpers.hasPreserveWhitespace;
45  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isEndElement;
46  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isGraphicsProperty;
47  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isLineBreakStartEvent;
48  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isNoBreakHyphenStartEvent;
49  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isPageBreak;
50  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isParagraphEndEvent;
51  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isParagraphPropertiesStartEvent;
52  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isParagraphStartEvent;
53  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isRunPropsStartEvent;
54  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isRunStartEvent;
55  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isTabStartEvent;
56  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isTextPath;
57  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isTextStartEvent;
58  import static net.sf.okapi.filters.openxml.XMLEventHelpers.isWhitespace;
59  
60  /**
61   * @version okapi v1.47.0
62   * SV: Added extraction of image alt text 
63   */
64  class RunParser implements Parser<RunBuilder> {
65      private final Logger LOGGER = LoggerFactory.getLogger(getClass());
66  
67      private static final String UNEXPECTED_STRUCTURE = "Unexpected structure: ";
68  
69      private static final int COMPLEX_FIELD_INITIAL_NESTING_LEVEL = 1;
70  
71      private static final QName COMPLEX_FIELD_CHARACTER_TYPE = Namespaces.WordProcessingML.getQName("fldCharType");
72      private static final String COMPLEX_FIELD_CHARACTER = "fldChar";
73      private static final String COMPLEX_FIELD_INSTRUCTION = "instrText";
74      private static final String BEGIN = "begin";
75      private static final String SEPARATE = "separate";
76      private static final String END = "end";
77      private static final String EMPTY = "";
78  
79      private StartElementContext startElementContext;
80      private final IdGenerator nestedTextualIds;
81      private final StyleDefinitions styleDefinitions;
82      private final StyleOptimisation styleOptimisation;
83      private final NumberingDefinitions numberingDefinitions;
84      private final ContentCategoriesDetection contentCategoriesDetection;
85      private final String paragraphStyle;
86  
87      private final RunBuilder runBuilder;
88  
89      private final StrippableAttributes drawingRunPropertiesStrippableAttributes;
90      private final StrippableAttributes wordRunRevisions;
91      private final StrippableAttributes wordParagraphRevisions;
92  
93      private final SkippableElements defaultSkippableElements;
94      private final SkippableElements blockPropertiesSkippableElements;
95      private final RunSkippableElements runSkippableElements;
96      private final BlockSkippableElements blockSkippableElements;
97  
98      private int complexFieldNestingLevel;
99  
100     RunParser(
101         final StartElementContext startElementContext,
102         final IdGenerator nestedTextualIds,
103         final StyleDefinitions styleDefinitions,
104         final StyleOptimisation styleOptimisation,
105         final NumberingDefinitions numberingDefinitions,
106         final ContentCategoriesDetection contentCategoriesDetection,
107         final String paragraphStyle,
108         final boolean hidden
109     ) {
110         this.drawingRunPropertiesStrippableAttributes = new StrippableAttributes.DrawingRunProperties(
111             startElementContext.getConditionalParameters(),
112             startElementContext.getEventFactory()
113         );
114         this.wordRunRevisions = new StrippableAttributes.WordRunRevisions(
115             startElementContext.getEventFactory()
116         );
117         this.wordParagraphRevisions = new StrippableAttributes.WordParagraphRevisions(
118             startElementContext.getEventFactory()
119         );
120 
121         this.startElementContext = new StartElementContext(
122             this.wordRunRevisions.strip(startElementContext.getStartElement()),
123             startElementContext
124         );
125         this.nestedTextualIds = nestedTextualIds;
126         this.styleDefinitions = styleDefinitions;
127         this.styleOptimisation = styleOptimisation;
128         this.numberingDefinitions = numberingDefinitions;
129         this.contentCategoriesDetection = contentCategoriesDetection;
130         this.paragraphStyle = paragraphStyle;
131 
132         runBuilder = new RunBuilder(this.startElementContext, styleDefinitions);
133         runBuilder.setHidden(hidden);
134 
135         defaultSkippableElements = new SkippableElements.Default();
136         blockPropertiesSkippableElements = new SkippableElements.RevisionProperty(
137             new SkippableElements.Property(
138                 new SkippableElements.Default(
139                     SkippableElement.RunProperty.RUN_PROPERTY_RTL_DML,
140                     SkippableElement.RunProperty.RUN_PROPERTY_LANGUAGE,
141                     SkippableElement.RevisionProperty.RUN_PROPERTY_INSERTED_PARAGRAPH_MARK,
142                     SkippableElement.RevisionProperty.RUN_PROPERTY_MOVED_PARAGRAPH_TO,
143                     SkippableElement.RevisionProperty.RUN_PROPERTY_MOVED_PARAGRAPH_FROM,
144                     SkippableElement.RevisionProperty.PARAGRAPH_PROPERTIES_CHANGE,
145                     SkippableElement.RevisionProperty.RUN_PROPERTIES_CHANGE
146                 ),
147                 startElementContext.getConditionalParameters()
148             ),
149             startElementContext.getConditionalParameters()
150         );
151         runSkippableElements = new RunSkippableElements(startElementContext);
152         blockSkippableElements = new BlockSkippableElements(startElementContext);
153     }
154 
155     public RunBuilder parse() throws XMLStreamException {
156         log("startRun: " + startElementContext.getStartElement());
157 
158         // rPr is either the first child, or not present (section 17.3.2)
159         XMLEvent firstChild = startElementContext.getEventReader().nextTag();
160         if (isRunPropsStartEvent(firstChild)) {
161             parseRunProperties(firstChild.asStartElement());
162         } else if (isEndElement(firstChild, startElementContext.getStartElement())) {
163             // Empty run!
164             return endRunParsing(firstChild.asEndElement());
165         } else {
166             // No properties section
167             startRunParsing(firstChild);
168             if ((paragraphStyle == null || paragraphStyle.isEmpty()) &&
169                 !startElementContext.getConditionalParameters().getTranslateWordInExcludeStyleMode() && !runBuilder.isHidden()) {
170                 runBuilder.setHidden(true);
171             }
172             if (!startElementContext.getConditionalParameters().getTranslateWordInExcludeHighlightMode() && !runBuilder.isHidden()) {
173                 runBuilder.setHidden(true);
174             }
175         }
176         while (startElementContext.getEventReader().hasNext()) {
177             XMLEvent e = startElementContext.getEventReader().nextEvent();
178             log("processRun: " + e);
179             if (isEndElement(e, startElementContext.getStartElement())) {
180                 return endRunParsing(e.asEndElement());
181             } else {
182                 startRunParsing(e);
183             }
184         }
185         throw new IllegalStateException("Invalid content? Unterminated run");
186     }
187 
188     private RunBuilder endRunParsing(EndElement e) {
189         runBuilder.flushText();
190         runBuilder.flushMarkup();
191         this.runBuilder.setEndEvent(e);
192         clarifyRunProperties();
193         return this.runBuilder;
194     }
195 
196     private void clarifyRunProperties() {
197         final Set<QName> skippableProperties = new HashSet<>();
198         if (this.startElementContext.getConditionalParameters().getCleanupAggressively()) {
199             // XXX This is pretty hacky.
200             // Recalculate the properties now that consolidation has already happened.
201             // This is required in order to properly handle the aggressive-mode trimming
202             // of the vertAlign property, which is only done if there's no text in the
203             // run.  Whether or not text is present can only be correctly calculated
204             // -after- other run merging has already taken place.
205             if (!this.runBuilder.nonWhitespaceTextPresent()) {
206                 skippableProperties.add(SkippableElement.RunProperty.RUN_PROPERTY_VERTICAL_ALIGNMENT_WPML.toName());
207                 skippableProperties.add(SkippableElement.RunProperty.RUN_PROPERTY_VERTICAL_ALIGNMENT_SML.toName());
208             }
209         }
210         final String runText = this.runBuilder.firstRunText();
211         final RunProperties preCombinedRunProperties = this.runBuilder.preCombinedRunProperties(this.paragraphStyle);
212         final RunProperties runProperties = this.runBuilder.getRunProperties();
213         final RunFonts runFonts;
214         if (runProperties.contains(RunFonts.NAME)) {
215             runFonts = runProperties.fontsRunProperty().getRunFonts();
216         } else {
217             runFonts = new RunFonts(preCombinedRunProperties.fontsRunProperty().getRunFonts());
218         }
219         if (!runText.isEmpty()) {
220             this.contentCategoriesDetection.performFor(runFonts, runText);
221         }
222         this.runBuilder.detectedRunFonts(runFonts);
223         if (!runFonts.containsDetectedNonComplexScriptContentCategories()) {
224             if (canBeSkipped(SkippableElement.RunProperty.RUN_PROPERTY_BOLD.toName(), preCombinedRunProperties, runProperties)) {
225                 skippableProperties.add(SkippableElement.RunProperty.RUN_PROPERTY_BOLD.toName());
226             }
227             if (canBeSkipped(SkippableElement.RunProperty.RUN_PROPERTY_ITALICS.toName(), preCombinedRunProperties, runProperties)) {
228                 skippableProperties.add(SkippableElement.RunProperty.RUN_PROPERTY_ITALICS.toName());
229             }
230             if (canBeSkipped(SkippableElement.RunProperty.RUN_PROPERTY_FONT_SIZE.toName(), preCombinedRunProperties, runProperties)) {
231                 skippableProperties.add(SkippableElement.RunProperty.RUN_PROPERTY_FONT_SIZE.toName());
232             }
233         }
234         if (!runFonts.containsDetectedComplexScriptContentCategories()) {
235             if (canBeSkipped(SkippableElement.RunProperty.RUN_PROPERTY_COMPLEX_SCRIPT_BOLD.toName(), preCombinedRunProperties, runProperties)) {
236                 skippableProperties.add(SkippableElement.RunProperty.RUN_PROPERTY_COMPLEX_SCRIPT_BOLD.toName());
237             }
238             if (canBeSkipped(SkippableElement.RunProperty.RUN_PROPERTY_COMPLEX_SCRIPT_ITALICS.toName(), preCombinedRunProperties, runProperties)) {
239                 skippableProperties.add(SkippableElement.RunProperty.RUN_PROPERTY_COMPLEX_SCRIPT_ITALICS.toName());
240             }
241             if (canBeSkipped(SkippableElement.RunProperty.RUN_PROPERTY_COMPLEX_SCRIPT_FONT_SIZE.toName(), preCombinedRunProperties, runProperties)) {
242                 skippableProperties.add(SkippableElement.RunProperty.RUN_PROPERTY_COMPLEX_SCRIPT_FONT_SIZE.toName());
243             }
244         }
245         if (!skippableProperties.isEmpty()) {
246             this.runBuilder.setRunProperties(runProperties.filteredBy(p -> !skippableProperties.contains(p.getName())));
247         }
248         this.runBuilder.refineCombinedRunProperties(this.paragraphStyle, runFonts);
249     }
250 
251     private static boolean canBeSkipped(
252         final QName name,
253         final RunProperties preCombinedRunProperties,
254         final RunProperties runProperties
255     ) {
256         final boolean v;
257         if (preCombinedRunProperties.contains(name) && runProperties.contains(name)) {
258             final RunProperty pcrp = (RunProperty) preCombinedRunProperties.filteredBy(p -> p.getName().equals(name)).properties().get(0);
259             final RunProperty rp = (RunProperty) runProperties.filteredBy(p -> p.getName().equals(name)).properties().get(0);
260             v = pcrp.equals(rp);
261         } else {
262             v = true;
263         }
264         return v;
265     }
266 
267     private void startRunParsing(final XMLEvent e) throws XMLStreamException {
268         if (isParagraphStartEvent(e)) {
269             parseNestedBlock(e.asStartElement());
270         }
271         // XXX I need to make sure I don't try to merge this thing
272         else if (isComplexFieldBegin(e)) {
273             parseComplexField(e.asStartElement());
274             this.runBuilder.setContainsComplexFields(true);
275         } else {
276             if (!parseSkippableElements(e)) {
277                 parseContent(processTranslatableAttributes(e), startElementContext.getEventReader());
278             }
279         }
280     }
281 
282     private void parseRunProperties(StartElement startElement) throws XMLStreamException {
283         StartElementContext runPropertiesElementContext = new StartElementContext(startElement, runBuilder.getStartElementContext());
284         final RunProperties direct = new RunPropertiesParser(runPropertiesElementContext, runSkippableElements).parse();
285         final String runStyle = null == direct.getRunStyleProperty()
286             ? null
287             : direct.getRunStyleProperty().value();
288         runBuilder.setRunProperties(
289             direct.minified(
290                 this.styleDefinitions.combinedRunProperties(
291                     this.paragraphStyle,
292                     runStyle,
293                     new RunProperties.Default(
294                         runPropertiesElementContext.getEventFactory(),
295                         startElement.getName().getPrefix(),
296                         startElement.getName().getNamespaceURI(),
297                         startElement.getName().getLocalPart()
298                     )
299                 )
300             )
301         );
302         // Handle Style exclusion or inclusion
303         if (runStyle != null) {
304             runBuilder.setRunStyle(runStyle);
305             if (startElementContext.getConditionalParameters().getTranslateWordInExcludeStyleMode()) {
306                 runBuilder.setHidden(startElementContext.getConditionalParameters().isWordExcludedStyle(runStyle));
307             } else {
308                 runBuilder.setHidden(!startElementContext.getConditionalParameters().isWordExcludedStyle(runStyle));
309             }
310         }
311         // Handle Highlight exclusion or inclusion
312         RunProperty.HighlightRunProperty highlightRunProperty = runBuilder.getRunProperties().getHighlightProperty();
313         if (highlightRunProperty != null) {
314             if (startElementContext.getConditionalParameters().getTranslateWordInExcludeHighlightMode() && !runBuilder.isHidden()) {
315                 runBuilder.setHidden(startElementContext.getConditionalParameters().isWordHighlight(highlightRunProperty.asColorValue()));
316             } else {
317                 runBuilder.setHidden(!startElementContext.getConditionalParameters().isWordHighlight(highlightRunProperty.asColorValue()));
318             }
319         }
320         // Handle Color Exclude
321         RunProperty.ColorRunProperty colorRunProperty = runBuilder.getRunProperties().getColorRunProperty();
322         if (colorRunProperty != null) {
323             if (startElementContext.getConditionalParameters().getTranslateWordExcludeColors() && !runBuilder.isHidden()) {
324                 runBuilder.setHidden(startElementContext.getConditionalParameters().isWordExcludedColor(colorRunProperty.asColorValue()));
325             }
326             if (startElementContext.getConditionalParameters().getTranslateExcelExcludeColors() && !runBuilder.isHidden()) {
327                 runBuilder.setHidden(startElementContext.getConditionalParameters().isExcelExcludedColor(colorRunProperty.asColorValue()));
328             }
329         }
330     }
331 
332     private void parseNestedBlock(final StartElement startElement) throws XMLStreamException {
333         log("Nested block start event: " + startElement);
334         runBuilder.flushText();
335         StartElementContext blockElementContext = new StartElementContext(startElement, startElementContext);
336 
337         final StyleDefinitions styleDefinitions;
338         final StyleOptimisation styleOptimisation;
339         final ContentCategoriesDetection contentCategoriesDetection;
340         if (Namespace.PREFIX_A.equals(startElement.getName().getPrefix())) {
341             styleDefinitions = new StyleDefinitions.Empty();
342             styleOptimisation = new StyleOptimisation.Default(
343                 new StyleOptimisation.Bypass(),
344                 this.startElementContext.getConditionalParameters(),
345                 this.startElementContext.getEventFactory(),
346                 this.startElementContext.presetColorValues(),
347                 this.startElementContext.highlightColorValues(),
348                 this.startElementContext.systemColorValues(),
349                 this.startElementContext.indexedColors(),
350                 this.startElementContext.theme(),
351                 new QName(startElement.getName().getNamespaceURI(), ParagraphBlockProperties.PPR, startElement.getName().getPrefix()),
352                 new QName(startElement.getName().getNamespaceURI(), RunProperties.DEF_RPR, startElement.getName().getPrefix()),
353                 Collections.emptyList(),
354                 styleDefinitions
355             );
356             contentCategoriesDetection = new ContentCategoriesDetection.NonApplicable();
357         } else {
358             styleDefinitions = runBuilder.getStyleDefinitions();
359             styleOptimisation = this.styleOptimisation;
360             contentCategoriesDetection = this.contentCategoriesDetection;
361         }
362         BlockParser nestedBlockParser = new BlockParser(
363             blockElementContext,
364             nestedTextualIds,
365             styleDefinitions,
366             styleOptimisation,
367             numberingDefinitions,
368             contentCategoriesDetection
369         );
370         Block nested = nestedBlockParser.parse();
371         clarifyStartElementContextWith(nested.deferredEvents());
372         nested.optimiseStyles();
373         runBuilder.setContainsNestedItems(true);
374         if (nested.hasVisibleRunContent()) {
375             // Create a reference to mark the location of the nested block
376             runBuilder.addToMarkup(startElementContext.getEventFactory().createCharacters(
377                     TextFragment.makeRefMarker(nestedTextualIds.createId())));
378             runBuilder.getNestedTextualItems().add(nested);
379         } else {
380             // Empty block, we don't need to expose it after all
381             for (XMLEvent nestedEvent : nested.getEvents()) {
382                 runBuilder.addToMarkup(nestedEvent);
383             }
384             // However, we do need to preserve anything it references that's translatable
385             for (Chunk chunk : nested.getChunks()) {
386                 if (chunk instanceof Run) {
387                     runBuilder.getNestedTextualItems().addAll(((Run) chunk).getNestedTextualItems());
388                 } else if (chunk instanceof RunContainer) {
389                     for (Chunk nestedChunk : ((RunContainer) chunk).getChunks()) {
390                         if (nestedChunk instanceof Run) {
391                             runBuilder.getNestedTextualItems().addAll(((Run) nestedChunk).getNestedTextualItems());
392                         }
393                     }
394                 }
395             }
396         }
397     }
398 
399     private void clarifyStartElementContextWith(final Collection<XMLEvent> deferredEvents) {
400         if (!deferredEvents.isEmpty()) {
401             this.startElementContext = new StartElementContext(
402                 this.startElementContext.getStartElement(),
403                 this.startElementContext.getParentStartElement(),
404                 new PrioritisedXMLEventReader(
405                     new ConsumableXMLEventsReader(new XMLEventsReader(deferredEvents)),
406                     this.startElementContext.getEventReader()
407                 ),
408                 this.startElementContext.presetColorValues(),
409                 this.startElementContext.highlightColorValues(),
410                 this.startElementContext.systemColorValues(),
411                 this.startElementContext.indexedColors(),
412                 this.startElementContext.theme(),
413                 this.startElementContext.getEventFactory(),
414                 this.startElementContext.getConditionalParameters()
415             );
416         }
417     }
418 
419     private void parseComplexField(final StartElement startElement) throws XMLStreamException {
420         String code = EMPTY;
421         boolean extractable = false;
422         boolean atComplexFieldResult = false;
423         final Deque<XMLEvent> deferredEvents = new LinkedList<>();
424 
425         this.complexFieldNestingLevel++;
426         parseComplexFieldBegin(startElement);
427 
428         while (startElementContext.getEventReader().hasNext()) {
429             final XMLEvent e = startElementContext.getEventReader().nextEvent();
430             if (isComplexFieldEnd(e)) {
431                 if (!extractable) {
432                     parseSimpleElement(e.asStartElement());
433                 } else {
434                     endComplexFieldParsing(e.asStartElement(), deferredEvents);
435                 }
436                 this.complexFieldNestingLevel--;
437                 return;
438             }
439             if (isComplexFieldInstruction(e)) {
440                 final String prevCode = code;
441                 code = parseComplexFieldCode(e.asStartElement());
442                 if (prevCode.isEmpty()) {
443                     extractable = startElementContext.getConditionalParameters().tsComplexFieldDefinitionsToExtract.contains(code);
444                 }
445                 continue;
446             }
447             if (isComplexFieldSeparate(e)) {
448                 parseSimpleElement(e.asStartElement());
449                 atComplexFieldResult = true;
450                 continue;
451             }
452             if (isComplexFieldBegin(e)) {
453                 if (extractable && !deferredEvents.isEmpty()) {
454                     parseContentFrom(deferredEvents);
455                 }
456                 parseComplexField(e.asStartElement());
457                 continue;
458             }
459             if (!extractable || !atComplexFieldResult) {
460                 if (parseSkippableElements(e)) {
461                     continue;
462                 }
463                 this.runBuilder.addToMarkup(stripRevisionAttributes(e));
464                 continue;
465             }
466             if (isParagraphEndEvent(e)) {
467                 // stick to the latest available paragraph
468                 if (!deferredEvents.isEmpty()) {
469                     parseContentFrom(deferredEvents);
470                 }
471                 deferredEvents.add(e);
472                 continue;
473             }
474             if (!deferredEvents.isEmpty()) {
475                 if (isParagraphPropertiesStartEvent(e)) {
476                     StartElementContext blockPropertiesElementContext = new StartElementContext(
477                         e.asStartElement(),
478                         this.startElementContext
479                     );
480                     final ParagraphBlockProperties blockProperties = new MarkupComponentParser()
481                             .parseParagraphBlockProperties(
482                                 blockPropertiesElementContext,
483                                 this.drawingRunPropertiesStrippableAttributes,
484                                 this.blockPropertiesSkippableElements
485                             );
486                     deferredEvents.addAll(blockProperties.getEvents());
487                 } else {
488                     if (parseSkippableElements(e)) {
489                         continue;
490                     }
491                     deferredEvents.add(e);
492                 }
493                 continue;
494             }
495             parseContent(
496                 processTranslatableAttributes(stripRevisionAttributes(e)),
497                 startElementContext.getEventReader()
498             );
499         }
500     }
501 
502     private static boolean isComplexFieldBegin(XMLEvent e) {
503         return XMLEventHelpers.isStartElement(e, COMPLEX_FIELD_CHARACTER)
504                 && BEGIN.equals(XMLEventHelpers.getAttributeValue(e.asStartElement(), COMPLEX_FIELD_CHARACTER_TYPE));
505     }
506 
507     private static boolean isComplexFieldInstruction(XMLEvent e) {
508         return XMLEventHelpers.isStartElement(e, COMPLEX_FIELD_INSTRUCTION);
509     }
510 
511     private static boolean isComplexFieldSeparate(XMLEvent e) {
512         return XMLEventHelpers.isStartElement(e, COMPLEX_FIELD_CHARACTER)
513                 && SEPARATE.equals(XMLEventHelpers.getAttributeValue(e.asStartElement(), COMPLEX_FIELD_CHARACTER_TYPE));
514     }
515 
516     private static boolean isComplexFieldEnd(XMLEvent e) {
517         return XMLEventHelpers.isStartElement(e, COMPLEX_FIELD_CHARACTER)
518                 && END.equals(XMLEventHelpers.getAttributeValue(e.asStartElement(), COMPLEX_FIELD_CHARACTER_TYPE));
519     }
520 
521     private void parseComplexFieldBegin(final StartElement startElement) throws XMLStreamException {
522         this.runBuilder.addToMarkup(startElement);
523         while (this.startElementContext.getEventReader().hasNext()) {
524             final XMLEvent event = this.startElementContext.getEventReader().nextEvent();
525             this.runBuilder.addToMarkup(event);
526             if (event.isEndElement() && event.asEndElement().getName().equals(startElement.getName())) {
527                 return;
528             }
529         }
530         throw new IllegalStateException(UNEXPECTED_STRUCTURE + startElement);
531     }
532 
533     private void parseSimpleElement(final StartElement startElement) throws XMLStreamException {
534         this.runBuilder.addToMarkup(startElement);
535         final XMLEvent event = this.startElementContext.getEventReader().nextEvent();
536 
537         if (!event.isEndElement() || !event.asEndElement().getName().equals(startElement.getName())) {
538             throw new IllegalStateException(UNEXPECTED_STRUCTURE + event);
539         }
540         this.runBuilder.addToMarkup(event);
541     }
542 
543     private boolean parseSkippableElements(final XMLEvent e) throws XMLStreamException {
544         if (runSkippableElements.skip(e)) {
545             return true;
546         } else if (blockSkippableElements.skip(e)) {
547             return true;
548         }
549         return false;
550     }
551 
552     private void endComplexFieldParsing(final StartElement startElement, final Deque<XMLEvent> deferredEvents) throws XMLStreamException {
553         if (goesAfterAnotherRun(deferredEvents)) {
554             // preserve the boundary place
555             parseContentFrom(deferredEvents);
556             parseSimpleElement(startElement);
557         } else {
558             final Collection<XMLEvent> subtractedEvents = subtractAllFromLastOpeningRun(deferredEvents);
559             parseContentFrom(subtractedEvents);
560             parseSimpleElement(startElement);
561             if (COMPLEX_FIELD_INITIAL_NESTING_LEVEL == this.complexFieldNestingLevel) {
562                 this.runBuilder.addDeferredEvents(deferredEvents);
563             } else {
564                 parseToFirstClosingRun();
565                 parseContentFrom(deferredEvents);
566             }
567         }
568     }
569 
570     private boolean goesAfterAnotherRun(final Deque<XMLEvent> deferredEvents) {
571         final Iterator<XMLEvent> deferredEventsIterator = deferredEvents.descendingIterator();
572 
573         while (deferredEventsIterator.hasNext()) {
574             final XMLEvent event = deferredEventsIterator.next();
575 
576             if (event.isEndElement() && XMLEventHelpers.LOCAL_RUN.equals(event.asEndElement().getName().getLocalPart())) {
577                 return true;
578             }
579         }
580 
581         return false;
582     }
583 
584     private void parseContentFrom(final Collection<XMLEvent> deferredEvents) throws XMLStreamException {
585         final XMLEventReader eventReader = new ConsumableXMLEventsReader(new XMLEventsReader(deferredEvents));
586         while (eventReader.hasNext()) {
587             parseContent(
588                     processTranslatableAttributes(stripRevisionAttributes(eventReader.nextEvent())),
589                     eventReader
590             );
591         }
592     }
593 
594     private Deque<XMLEvent> subtractAllFromLastOpeningRun(final Deque<XMLEvent> deferredEvents) {
595         final Deque<XMLEvent> subtraction = new LinkedList<>();
596         final Iterator<XMLEvent> deferredEventsIterator = deferredEvents.descendingIterator();
597 
598         while (deferredEventsIterator.hasNext()) {
599             final XMLEvent event = deferredEventsIterator.next();
600             subtraction.addFirst(event);
601             deferredEventsIterator.remove();
602             if (isRunStartEvent(event)) {
603                 break;
604             }
605         }
606 
607         return subtraction;
608     }
609 
610     private void parseToFirstClosingRun() throws XMLStreamException {
611         while (this.startElementContext.getEventReader().hasNext()) {
612             final XMLEvent event = this.startElementContext.getEventReader().nextEvent();
613             parseContent(
614                 processTranslatableAttributes(stripRevisionAttributes(event)),
615                 this.startElementContext.getEventReader()
616             );
617             if (event.isEndElement()
618                     && XMLEventHelpers.LOCAL_RUN.equals(event.asEndElement().getName().getLocalPart())) {
619                 return;
620             }
621         }
622     }
623 
624     private String parseComplexFieldCode(final StartElement startElement) throws XMLStreamException {
625         this.runBuilder.addToMarkup(startElement);
626         String codeName = EMPTY;
627         while (this.startElementContext.getEventReader().hasNext()) {
628             final XMLEvent event = this.startElementContext.getEventReader().nextEvent();
629             this.runBuilder.addToMarkup(event);
630 
631             if (event.isEndElement() && event.asEndElement().getName().equals(startElement.getName())) {
632                 return codeName;
633             }
634 
635             if (codeName.isEmpty() && event.isCharacters()) {
636                 codeName = complexFieldCodeName(event.asCharacters());
637             }
638         }
639         throw new IllegalStateException(UNEXPECTED_STRUCTURE + startElement);
640     }
641 
642     private String complexFieldCodeName(final Characters characters) {
643         // get the field definition out of the field code string
644         String data = characters.getData().trim();
645         int fieldCodeNameLength = data.indexOf(" ");
646 
647         String fieldCodeName;
648         if (fieldCodeNameLength > 0) {
649             fieldCodeName = data.substring(0, fieldCodeNameLength);
650         } else {
651             fieldCodeName = data;
652         }
653         return fieldCodeName;
654     }
655 
656     private XMLEvent stripRevisionAttributes(XMLEvent e) {
657         if (isParagraphStartEvent(e)) {
658             return wordParagraphRevisions.strip(e.asStartElement());
659         } else if (isRunStartEvent(e)) {
660             return wordRunRevisions.strip(e.asStartElement());
661         } else {
662             return e;
663         }
664     }
665 
666     private void parseContent(final XMLEvent e, final XMLEventReader eventReader) throws XMLStreamException {
667 
668         if (isTextStartEvent(e)) {
669 
670             runBuilder.flushMarkup();
671             parseText(e.asStartElement(), eventReader);
672 
673         } else if (startElementContext.getConditionalParameters().getAddTabAsCharacter() && isTabStartEvent(e) && !runBuilder.isHidden()) {
674 
675             runBuilder.flushMarkup();
676             runBuilder.addText("\t");
677             defaultSkippableElements.skip(new StartElementContext(
678                 e.asStartElement(),
679                 eventReader,
680                 startElementContext.presetColorValues(),
681                 startElementContext.highlightColorValues(),
682                 startElementContext.systemColorValues(),
683                 startElementContext.indexedColors(),
684                 startElementContext.theme(),
685                 null,
686                 startElementContext.getConditionalParameters()
687             ));
688 
689         } else if (startElementContext.getConditionalParameters().getAddLineSeparatorCharacter() && isLineBreakStartEvent(e) && !isPageBreak(e.asStartElement()) && !runBuilder.isHidden()) {
690 
691             runBuilder.flushMarkup();
692             char replacement = startElementContext.getConditionalParameters().getLineSeparatorReplacement();
693             runBuilder.addText(String.valueOf(replacement));
694             defaultSkippableElements.skip(new StartElementContext(
695                 e.asStartElement(),
696                 eventReader,
697                 startElementContext.presetColorValues(),
698                 startElementContext.highlightColorValues(),
699                 startElementContext.systemColorValues(),
700                 startElementContext.indexedColors(),
701                 startElementContext.theme(),
702                 null,
703                 startElementContext.getConditionalParameters()
704             ));
705 
706         } else if (startElementContext.getConditionalParameters().getReplaceNoBreakHyphenTag() && isNoBreakHyphenStartEvent(e)) {
707 
708             runBuilder.flushMarkup();
709             runBuilder.addText(LOCAL_REGULAR_HYPHEN_VALUE);
710             defaultSkippableElements.skip(new StartElementContext(
711                 e.asStartElement(),
712                 eventReader,
713                 startElementContext.presetColorValues(),
714                 startElementContext.highlightColorValues(),
715                 startElementContext.systemColorValues(),
716                 startElementContext.indexedColors(),
717                 startElementContext.theme(),
718                 null,
719                 startElementContext.getConditionalParameters()
720             ));
721 
722         } else if (!isWhitespace(e) || runBuilder.preservingWhitespace()) {
723 
724             // Real text should have been handled above.  Most whitespace is ignorable,
725             // but if we're in a preserve-whitespace section, we need to save it (eg
726             // for w:instrText, which isn't translatable but needs to be preserved).
727             runBuilder.flushText();
728             runBuilder.setTextPreservingWhitespace(false);
729 
730             if (XMLEventHelpers.isParagraphStartEvent(e)) {
731                 runBuilder.addToMarkup(
732                     new MarkupComponent.StyledStart(
733                         startElementContext.getEventFactory(),
734                         styleDefinitions,
735                         e.asStartElement()
736                     )
737                 );
738             } else if (XMLEventHelpers.isParagraphPropertiesStartEvent(e)) {
739                 runBuilder.addToMarkup(
740                     new MarkupComponentParser().parseParagraphBlockProperties(
741                         new StartElementContext(
742                             e.asStartElement(),
743                             eventReader,
744                             startElementContext.presetColorValues(),
745                             startElementContext.highlightColorValues(),
746                             startElementContext.systemColorValues(),
747                             startElementContext.indexedColors(),
748                             startElementContext.theme(),
749                             startElementContext.getEventFactory(),
750                             startElementContext.getConditionalParameters()
751                         ),
752                         this.drawingRunPropertiesStrippableAttributes,
753                         this.blockPropertiesSkippableElements
754                     )
755                 );
756             } else if (XMLEventHelpers.isParagraphEndEvent(e)) {
757                 runBuilder.addToMarkup(
758                     MarkupComponentFactory.createEndMarkupComponent(
759                         e.asEndElement()
760                     )
761                 );
762             } else if (!parseSkippableElements(e)) {
763                 runBuilder.addToMarkup(e);
764             }
765         }
766     }
767 
768     private void parseText(StartElement startEvent, XMLEventReader events) throws XMLStreamException {
769         // Merge the preserve whitespace flag
770         runBuilder.setTextPreservingWhitespace(runBuilder.isTextPreservingWhitespace() || hasPreserveWhitespace(startEvent));
771 
772         while (events.hasNext()) {
773             XMLEvent e = events.nextEvent();
774             if (isEndElement(e, startEvent)) {
775                 return;
776             } else if (e.isCharacters()) {
777                 String text = e.asCharacters().getData();
778                 if (text.trim().length() > 0) {
779                     runBuilder.nonWhitespaceTextPresent(true);
780                 }
781                 runBuilder.addText(text);
782             }
783         }
784     }
785 
786     // translatable attributes:
787     // wp:docPr/@name  if that option isn't set
788     // v:textpath/@string
789     private XMLEvent processTranslatableAttributes(XMLEvent e) {
790         if (!e.isStartElement()) return e;
791         StartElement startEl = e.asStartElement();
792         // I will need to
793         // - extract translatable attribute
794         // - create a new start event with all the attributes except for that one, which is replaced
795         if (isGraphicsProperty(startEl) && !startElementContext.getConditionalParameters().getTranslateWordExcludeGraphicMetaData()) {
796             startEl = processTranslatableAttribute(startEl, "name");
797             startEl = processTranslatableAttribute(startEl, "title");
798             startEl = processTranslatableAttribute(startEl, "descr");
799         } else if (isTextPath(startEl)) {
800             startEl = processTranslatableAttribute(startEl, "string");
801         }
802         return startEl;
803     }
804 
805     private StartElement processTranslatableAttribute(StartElement startEl, String attrName) {
806         List<Attribute> newAttrs = new ArrayList<>();
807         Iterator<?> it = startEl.getAttributes();
808         boolean dirty = false;
809         while (it.hasNext()) {
810             Attribute a = (Attribute) it.next();
811             if (a.getName().getLocalPart().equals(attrName)) {
812                 runBuilder.setContainsNestedItems(true);
813                 runBuilder.getNestedTextualItems().add(new TranslatableAttributeText(a.getValue()));
814                 newAttrs.add(startElementContext.getEventFactory().createAttribute(a.getName(),
815                         TextFragment.makeRefMarker(nestedTextualIds.createId())));
816                 dirty = true;
817             } else {
818                 newAttrs.add(a);
819             }
820         }
821         return dirty ?
822                 startElementContext.getEventFactory().createStartElement(startEl.getName(), newAttrs.iterator(), startEl.getNamespaces()) :
823                 startEl;
824     }
825 
826     private void log(String s) {
827         LOGGER.debug(s);
828     }
829 }