Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
208 / 208
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
Content
100.00% covered (success)
100.00%
208 / 208
100.00% covered (success)
100.00%
9 / 9
49
100.00% covered (success)
100.00%
1 / 1
 write
100.00% covered (success)
100.00%
64 / 64
100.00% covered (success)
100.00%
1 / 1
9
 writeAutoStyles
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 writeTextStyles
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
9
 getAutoStyles
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getContainerStyle
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
9
 getElementStyle
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
6
 getElementStyleField
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getElementStyleTextRun
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 collectTrackedChanges
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * This file is part of PHPWord - A pure PHP library for reading and writing
4 * word processing documents.
5 *
6 * PHPWord is free software distributed under the terms of the GNU Lesser
7 * General Public License version 3 as published by the Free Software Foundation.
8 *
9 * For the full copyright and license information, please read the LICENSE
10 * file that was distributed with this source code. For the full list of
11 * contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
12 *
13 * @see         https://github.com/PHPOffice/PHPWord
14 *
15 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
16 */
17
18namespace PhpOffice\PhpWord\Writer\ODText\Part;
19
20use PhpOffice\PhpWord\Element\AbstractContainer;
21use PhpOffice\PhpWord\Element\Field;
22use PhpOffice\PhpWord\Element\Image;
23use PhpOffice\PhpWord\Element\Table;
24use PhpOffice\PhpWord\Element\Text;
25use PhpOffice\PhpWord\Element\TextRun;
26use PhpOffice\PhpWord\Element\TrackChange;
27use PhpOffice\PhpWord\PhpWord;
28use PhpOffice\PhpWord\Shared\XMLWriter;
29use PhpOffice\PhpWord\Style;
30use PhpOffice\PhpWord\Style\Font;
31use PhpOffice\PhpWord\Style\Paragraph;
32use PhpOffice\PhpWord\Style\Table as TableStyle;
33use PhpOffice\PhpWord\Writer\ODText\Element\Container;
34use PhpOffice\PhpWord\Writer\ODText\Style\Paragraph as ParagraphStyleWriter;
35
36/**
37 * ODText content part writer: content.xml.
38 */
39class Content extends AbstractPart
40{
41    /**
42     * Auto style collection.
43     *
44     * Collect inline style information from section, image, and table elements
45     *
46     * @todo Merge font and paragraph styles
47     *
48     * @var array
49     */
50    private $autoStyles = ['Section' => [], 'Image' => [], 'Table' => []];
51
52    private $imageParagraphStyles = [];
53
54    /**
55     * Write part.
56     *
57     * @return string
58     */
59    public function write()
60    {
61        $xmlWriter = $this->getXmlWriter();
62        $phpWord = $this->getParentWriter()->getPhpWord();
63
64        $this->getAutoStyles($phpWord);
65
66        $xmlWriter->startDocument('1.0', 'UTF-8');
67        $xmlWriter->startElement('office:document-content');
68        $this->writeCommonRootAttributes($xmlWriter);
69        $xmlWriter->writeAttribute('xmlns:xforms', 'http://www.w3.org/2002/xforms');
70        $xmlWriter->writeAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema');
71        $xmlWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
72        $xmlWriter->writeAttribute('xmlns:field', 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0');
73        $xmlWriter->writeAttribute('xmlns:formx', 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0');
74
75        // Font declarations and automatic styles
76        $this->writeFontFaces($xmlWriter); // office:font-face-decls
77        $this->writeAutoStyles($xmlWriter); // office:automatic-styles
78
79        // Body
80        $xmlWriter->startElement('office:body');
81        $xmlWriter->startElement('office:text');
82
83        // Tracked changes declarations
84        $trackedChanges = [];
85        $sections = $phpWord->getSections();
86        foreach ($sections as $section) {
87            $this->collectTrackedChanges($section, $trackedChanges);
88        }
89        $xmlWriter->startElement('text:tracked-changes');
90        foreach ($trackedChanges as $trackedElement) {
91            $trackedChange = $trackedElement->getTrackChange();
92            $xmlWriter->startElement('text:changed-region');
93            $trackedChange->setElementId();
94            $xmlWriter->writeAttribute('text:id', $trackedChange->getElementId());
95
96            if (($trackedChange->getChangeType() == TrackChange::INSERTED)) {
97                $xmlWriter->startElement('text:insertion');
98            } elseif ($trackedChange->getChangeType() == TrackChange::DELETED) {
99                $xmlWriter->startElement('text:deletion');
100            }
101
102            $xmlWriter->startElement('office:change-info');
103            $xmlWriter->writeElement('dc:creator', $trackedChange->getAuthor());
104            if ($trackedChange->getDate() != null) {
105                $xmlWriter->writeElement('dc:date', $trackedChange->getDate()->format('Y-m-d\TH:i:s\Z'));
106            }
107            $xmlWriter->endElement(); // office:change-info
108            if ($trackedChange->getChangeType() == TrackChange::DELETED) {
109                $xmlWriter->writeElement('text:p', $trackedElement->getText());
110            }
111
112            $xmlWriter->endElement(); // text:insertion|text:deletion
113            $xmlWriter->endElement(); // text:changed-region
114        }
115        $xmlWriter->endElement(); // text:tracked-changes
116
117        // Sequence declarations
118        $sequences = ['Illustration', 'Table', 'Text', 'Drawing'];
119        $xmlWriter->startElement('text:sequence-decls');
120        foreach ($sequences as $sequence) {
121            $xmlWriter->startElement('text:sequence-decl');
122            $xmlWriter->writeAttribute('text:display-outline-level', 0);
123            $xmlWriter->writeAttribute('text:name', $sequence);
124            $xmlWriter->endElement();
125        }
126        $xmlWriter->endElement(); // text:sequence-decl
127
128        // Sections
129        $sections = $phpWord->getSections();
130        foreach ($sections as $section) {
131            $name = 'Section' . $section->getSectionId();
132            $xmlWriter->startElement('text:section');
133            $xmlWriter->writeAttribute('text:name', $name);
134            $xmlWriter->writeAttribute('text:style-name', $name);
135            $xmlWriter->startElement('text:p');
136            $xmlWriter->writeAttribute('text:style-name', 'SB' . $section->getSectionId());
137            $xmlWriter->endElement();
138
139            $containerWriter = new Container($xmlWriter, $section);
140            $containerWriter->setPart($this);
141            $containerWriter->write();
142
143            $xmlWriter->endElement(); // text:section
144        }
145
146        $xmlWriter->endElement(); // office:text
147        $xmlWriter->endElement(); // office:body
148
149        $xmlWriter->endElement(); // office:document-content
150
151        return $xmlWriter->getData();
152    }
153
154    /**
155     * Write automatic styles other than fonts and paragraphs.
156     *
157     * @since 0.11.0
158     */
159    private function writeAutoStyles(XMLWriter $xmlWriter): void
160    {
161        $xmlWriter->startElement('office:automatic-styles');
162
163        $this->writeTextStyles($xmlWriter);
164        foreach ($this->autoStyles as $element => $styles) {
165            $writerClass = 'PhpOffice\\PhpWord\\Writer\\ODText\\Style\\' . $element;
166            foreach ($styles as $style) {
167                /** @var \PhpOffice\PhpWord\Writer\ODText\Style\AbstractStyle $styleWriter Type hint */
168                $styleWriter = new $writerClass($xmlWriter, $style);
169                $styleWriter->write();
170            }
171        }
172
173        $xmlWriter->endElement(); // office:automatic-styles
174    }
175
176    /**
177     * Write automatic styles.
178     */
179    private function writeTextStyles(XMLWriter $xmlWriter): void
180    {
181        $styles = Style::getStyles();
182        $paragraphStyleCount = 0;
183
184        $style = new Paragraph();
185        $style->setStyleName('PB');
186        $style->setAuto();
187        $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
188        $styleWriter->write();
189
190        $sects = $this->getParentWriter()->getPhpWord()->getSections();
191        $countsects = count($sects);
192        for ($i = 0; $i < $countsects; ++$i) {
193            $iplus1 = $i + 1;
194            $style = new Paragraph();
195            $style->setStyleName("SB$iplus1");
196            $style->setAuto();
197            $pnstart = $sects[$i]->getStyle()->getPageNumberingStart();
198            $style->setNumLevel($pnstart);
199            $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
200            $styleWriter->write();
201        }
202
203        foreach ($styles as $style) {
204            $sty = (string) $style->getStyleName();
205            if (substr($sty, 0, 8) === 'Heading_') {
206                $style = new Paragraph();
207                $style->setStyleName('HD' . substr($sty, 8));
208                $style->setAuto();
209                $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
210                $styleWriter->write();
211                $style = new Paragraph();
212                $style->setStyleName('HE' . substr($sty, 8));
213                $style->setAuto();
214                $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
215                $styleWriter->write();
216            }
217        }
218
219        foreach ($styles as $style) {
220            if ($style->isAuto() === true) {
221                $styleClass = str_replace('\\Style\\', '\\Writer\\ODText\\Style\\', get_class($style));
222                if (class_exists($styleClass)) {
223                    /** @var \PhpOffice\PhpWord\Writer\ODText\Style\AbstractStyle $styleWriter Type hint */
224                    $styleWriter = new $styleClass($xmlWriter, $style);
225                    $styleWriter->write();
226                }
227                if ($style instanceof Paragraph) {
228                    ++$paragraphStyleCount;
229                }
230            }
231        }
232        foreach ($this->imageParagraphStyles as $style) {
233            $styleWriter = new \PhpOffice\PhpWord\Writer\ODText\Style\Paragraph($xmlWriter, $style);
234            $styleWriter->write();
235        }
236    }
237
238    /**
239     * Get automatic styles.
240     */
241    private function getAutoStyles(PhpWord $phpWord): void
242    {
243        $sections = $phpWord->getSections();
244        $paragraphStyleCount = 0;
245        $fontStyleCount = 0;
246        foreach ($sections as $section) {
247            $style = $section->getStyle();
248            $style->setStyleName("Section{$section->getSectionId()}");
249            $this->autoStyles['Section'][] = $style;
250            $this->getContainerStyle($section, $paragraphStyleCount, $fontStyleCount);
251        }
252    }
253
254    /**
255     * Get all styles of each elements in container recursively.
256     *
257     * Table style can be null or string of the style name
258     *
259     * @param \PhpOffice\PhpWord\Element\AbstractContainer $container
260     * @param int $paragraphStyleCount
261     * @param int $fontStyleCount
262     *
263     * @todo Simplify the logic
264     */
265    private function getContainerStyle($container, &$paragraphStyleCount, &$fontStyleCount): void
266    {
267        $elements = $container->getElements();
268        foreach ($elements as $element) {
269            if ($element instanceof TextRun) {
270                $this->getElementStyleTextRun($element, $paragraphStyleCount);
271                $this->getContainerStyle($element, $paragraphStyleCount, $fontStyleCount);
272            } elseif ($element instanceof Text) {
273                $this->getElementStyle($element, $paragraphStyleCount, $fontStyleCount);
274            } elseif ($element instanceof Field) {
275                $this->getElementStyleField($element, $fontStyleCount);
276            } elseif ($element instanceof Image) {
277                $style = $element->getStyle();
278                $style->setStyleName('fr' . $element->getMediaIndex());
279                $this->autoStyles['Image'][] = $style;
280                $sty = new \PhpOffice\PhpWord\Style\Paragraph();
281                $sty->setStyleName('IM' . $element->getMediaIndex());
282                $sty->setAuto();
283                $sty->setAlignment($style->getAlignment());
284                $this->imageParagraphStyles[] = $sty;
285            } elseif ($element instanceof Table) {
286                $style = $element->getStyle();
287                if (is_string($style)) {
288                    $style = Style::getStyle($style);
289                }
290                if ($style === null) {
291                    $style = new TableStyle();
292                }
293                $style->setStyleName($element->getElementId());
294                $style->setColumnWidths($element->findFirstDefinedCellWidths());
295                $this->autoStyles['Table'][] = $style;
296            }
297        }
298    }
299
300    /**
301     * Get style of individual element.
302     *
303     * @param \PhpOffice\PhpWord\Element\Text $element
304     * @param int $paragraphStyleCount
305     * @param int $fontStyleCount
306     */
307    private function getElementStyle($element, &$paragraphStyleCount, &$fontStyleCount): void
308    {
309        $fontStyle = $element->getFontStyle();
310        $paragraphStyle = $element->getParagraphStyle();
311        $phpWord = $this->getParentWriter()->getPhpWord();
312
313        if ($fontStyle instanceof Font) {
314            // Font
315            $name = $fontStyle->getStyleName();
316            if (!$name) {
317                ++$fontStyleCount;
318                $style = $phpWord->addFontStyle("T{$fontStyleCount}", $fontStyle, null);
319                $style->setAuto();
320                $style->setParagraph(null);
321                $element->setFontStyle("T{$fontStyleCount}");
322            } else {
323                $element->setFontStyle($name);
324            }
325        }
326        if ($paragraphStyle instanceof Paragraph) {
327            // Paragraph
328            $name = $paragraphStyle->getStyleName();
329            if (!$name) {
330                ++$paragraphStyleCount;
331                $style = $phpWord->addParagraphStyle("P{$paragraphStyleCount}", $paragraphStyle);
332                $style->setAuto();
333                $element->setParagraphStyle("P{$paragraphStyleCount}");
334            } else {
335                $element->setParagraphStyle($name);
336            }
337        } elseif ($paragraphStyle) {
338            ++$paragraphStyleCount;
339            $parstylename = "P$paragraphStyleCount" . "_$paragraphStyle";
340            $style = $phpWord->addParagraphStyle($parstylename, $paragraphStyle);
341            $style->setAuto();
342            $element->setParagraphStyle($parstylename);
343        }
344    }
345
346    /**
347     * Get font style of individual field element.
348     *
349     * @param \PhpOffice\PhpWord\Element\Field $element
350     * @param int $fontStyleCount
351     */
352    private function getElementStyleField($element, &$fontStyleCount): void
353    {
354        $fontStyle = $element->getFontStyle();
355        $phpWord = $this->getParentWriter()->getPhpWord();
356
357        if ($fontStyle instanceof Font) {
358            $name = $fontStyle->getStyleName();
359            if (!$name) {
360                ++$fontStyleCount;
361                $style = $phpWord->addFontStyle("T{$fontStyleCount}", $fontStyle, null);
362                $style->setAuto();
363                $style->setParagraph(null);
364                $element->setFontStyle("T{$fontStyleCount}");
365            } else {
366                $element->setFontStyle($name);
367            }
368        }
369    }
370
371    /**
372     * Get style of individual element.
373     *
374     * @param \PhpOffice\PhpWord\Element\TextRun $element
375     * @param int $paragraphStyleCount
376     */
377    private function getElementStyleTextRun($element, &$paragraphStyleCount): void
378    {
379        $paragraphStyle = $element->getParagraphStyle();
380        $phpWord = $this->getParentWriter()->getPhpWord();
381
382        if ($paragraphStyle instanceof Paragraph) {
383            // Paragraph
384            $name = $paragraphStyle->getStyleName();
385            if (!$name) {
386                ++$paragraphStyleCount;
387                $style = $phpWord->addParagraphStyle("P{$paragraphStyleCount}", $paragraphStyle);
388                $style->setAuto();
389                $element->setParagraphStyle("P{$paragraphStyleCount}");
390            } else {
391                $element->setParagraphStyle($name);
392            }
393        } elseif ($paragraphStyle) {
394            ++$paragraphStyleCount;
395            $parstylename = "P$paragraphStyleCount" . "_$paragraphStyle";
396            $style = $phpWord->addParagraphStyle($parstylename, $paragraphStyle);
397            $style->setAuto();
398            $element->setParagraphStyle($parstylename);
399        }
400    }
401
402    /**
403     * Finds all tracked changes.
404     *
405     * @param \PhpOffice\PhpWord\Element\AbstractElement[] $trackedChanges
406     */
407    private function collectTrackedChanges(AbstractContainer $container, &$trackedChanges = []): void
408    {
409        $elements = $container->getElements();
410        foreach ($elements as $element) {
411            if ($element->getTrackChange() != null) {
412                $trackedChanges[] = $element;
413            }
414            if (is_callable([$element, 'getElements'])) {
415                $this->collectTrackedChanges($element, $trackedChanges);
416            }
417        }
418    }
419}