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