Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.28% covered (success)
97.28%
358 / 368
71.43% covered (warning)
71.43%
10 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Content
97.28% covered (success)
97.28%
358 / 368
71.43% covered (warning)
71.43%
10 / 14
127
0.00% covered (danger)
0.00%
0 / 1
 write
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
1 / 1
11
 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
98.46% covered (success)
98.46%
64 / 65
0.00% covered (danger)
0.00%
0 / 1
26
 hasCellStyle
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
10
 applyTableStyleToCell
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
16.08
 setCellBorderIfUnset
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 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
 collectFormControls
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
7.14
 writeForms
91.14% covered (success)
91.14%
72 / 79
0.00% covered (danger)
0.00%
0 / 1
23.37
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\Cell as CellElement;
23use PhpOffice\PhpWord\Element\CheckBox;
24use PhpOffice\PhpWord\Element\Field;
25use PhpOffice\PhpWord\Element\FormField;
26use PhpOffice\PhpWord\Element\Image;
27use PhpOffice\PhpWord\Element\Line;
28use PhpOffice\PhpWord\Element\ListItem;
29use PhpOffice\PhpWord\Element\Row as RowElement;
30use PhpOffice\PhpWord\Element\SDT as SDTElement;
31use PhpOffice\PhpWord\Element\Shape;
32use PhpOffice\PhpWord\Element\Table;
33use PhpOffice\PhpWord\Element\Text;
34use PhpOffice\PhpWord\Element\TextBox;
35use PhpOffice\PhpWord\Element\TextRun;
36use PhpOffice\PhpWord\Element\TrackChange;
37use PhpOffice\PhpWord\PhpWord;
38use PhpOffice\PhpWord\Shared\XMLWriter;
39use PhpOffice\PhpWord\Style;
40use PhpOffice\PhpWord\Style\Cell as CellStyle;
41use PhpOffice\PhpWord\Style\Font;
42use PhpOffice\PhpWord\Style\Line as LineStyle;
43use PhpOffice\PhpWord\Style\Paragraph;
44use PhpOffice\PhpWord\Style\Table as TableStyle;
45use PhpOffice\PhpWord\Writer\ODText\Element\Container;
46use PhpOffice\PhpWord\Writer\ODText\Style\Paragraph as ParagraphStyleWriter;
47
48/**
49 * ODText content part writer: content.xml.
50 *
51 * @SuppressWarnings("PHPMD.CouplingBetweenObjects")
52 */
53class Content extends AbstractPart
54{
55    /**
56     * Auto style collection.
57     *
58     * Collect inline style information from section, image, and table elements
59     *
60     * @todo Merge font and paragraph styles
61     *
62     * @var array
63     */
64    private $autoStyles = ['Section' => [], 'Image' => [], 'Line' => [], 'Table' => [], 'Row' => [], 'Cell' => [], 'Shape' => [], 'TextBox' => []];
65
66    private $imageParagraphStyles = [];
67
68    /**
69     * Write part.
70     *
71     * @return string
72     */
73    public function write()
74    {
75        $xmlWriter = $this->getXmlWriter();
76        $phpWord = $this->getParentWriter()->getPhpWord();
77
78        $this->getAutoStyles($phpWord);
79
80        $xmlWriter->startDocument('1.0', 'UTF-8');
81        $xmlWriter->startElement('office:document-content');
82        $this->writeCommonRootAttributes($xmlWriter);
83        $xmlWriter->writeAttribute('xmlns:xforms', 'http://www.w3.org/2002/xforms');
84        $xmlWriter->writeAttribute('xmlns:xsd', 'http://www.w3.org/2001/XMLSchema');
85        $xmlWriter->writeAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
86        $xmlWriter->writeAttribute('xmlns:field', 'urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0');
87        $xmlWriter->writeAttribute('xmlns:formx', 'urn:openoffice:names:experimental:ooxml-odf-interop:xmlns:form:1.0');
88
89        // Font declarations and automatic styles
90        $this->writeFontFaces($xmlWriter); // office:font-face-decls
91        $this->writeAutoStyles($xmlWriter); // office:automatic-styles
92
93        // Body
94        $xmlWriter->startElement('office:body');
95        $xmlWriter->startElement('office:text');
96
97        $formControls = [];
98        foreach ($phpWord->getSections() as $section) {
99            $this->collectFormControls($section, $formControls);
100        }
101        $this->writeForms($xmlWriter, $formControls);
102
103        // Tracked changes declarations
104        $trackedChanges = [];
105        $sections = $phpWord->getSections();
106        foreach ($sections as $section) {
107            $this->collectTrackedChanges($section, $trackedChanges);
108        }
109        $xmlWriter->startElement('text:tracked-changes');
110        foreach ($trackedChanges as $trackedElement) {
111            $trackedChange = $trackedElement->getTrackChange();
112            $xmlWriter->startElement('text:changed-region');
113            $trackedChange->setElementId();
114            $xmlWriter->writeAttribute('text:id', $trackedChange->getElementId());
115
116            if (($trackedChange->getChangeType() == TrackChange::INSERTED)) {
117                $xmlWriter->startElement('text:insertion');
118            } elseif ($trackedChange->getChangeType() == TrackChange::DELETED) {
119                $xmlWriter->startElement('text:deletion');
120            }
121
122            $xmlWriter->startElement('office:change-info');
123            $xmlWriter->writeElement('dc:creator', $trackedChange->getAuthor());
124            if ($trackedChange->getDate() != null) {
125                $xmlWriter->writeElement('dc:date', $trackedChange->getDate()->format('Y-m-d\TH:i:s\Z'));
126            }
127            $xmlWriter->endElement(); // office:change-info
128            if ($trackedChange->getChangeType() == TrackChange::DELETED && method_exists($trackedElement, 'getText')) {
129                // @phpstan-ignore-next-line
130                $xmlWriter->writeElement('text:p', $trackedElement->getText());
131            }
132
133            $xmlWriter->endElement(); // text:insertion|text:deletion
134            $xmlWriter->endElement(); // text:changed-region
135        }
136        $xmlWriter->endElement(); // text:tracked-changes
137
138        // Sequence declarations
139        $sequences = ['Illustration', 'Table', 'Text', 'Drawing'];
140        $xmlWriter->startElement('text:sequence-decls');
141        foreach ($sequences as $sequence) {
142            $xmlWriter->startElement('text:sequence-decl');
143            $xmlWriter->writeAttribute('text:display-outline-level', 0);
144            $xmlWriter->writeAttribute('text:name', $sequence);
145            $xmlWriter->endElement();
146        }
147        $xmlWriter->endElement(); // text:sequence-decl
148
149        // Sections
150        $sections = $phpWord->getSections();
151        foreach ($sections as $section) {
152            $name = 'Section' . $section->getSectionId();
153            $xmlWriter->startElement('text:section');
154            $xmlWriter->writeAttribute('text:name', $name);
155            $xmlWriter->writeAttribute('text:style-name', $name);
156            $xmlWriter->startElement('text:p');
157            $xmlWriter->writeAttribute('text:style-name', 'SB' . $section->getSectionId());
158            $xmlWriter->endElement();
159
160            $containerWriter = new Container($xmlWriter, $section);
161            $containerWriter->setPart($this);
162            $containerWriter->write();
163
164            $xmlWriter->endElement(); // text:section
165        }
166
167        $xmlWriter->endElement(); // office:text
168        $xmlWriter->endElement(); // office:body
169
170        $xmlWriter->endElement(); // office:document-content
171
172        return $xmlWriter->getData();
173    }
174
175    /**
176     * Write automatic styles other than fonts and paragraphs.
177     *
178     * @since 0.11.0
179     */
180    private function writeAutoStyles(XMLWriter $xmlWriter): void
181    {
182        $xmlWriter->startElement('office:automatic-styles');
183
184        $this->writeTextStyles($xmlWriter);
185        foreach ($this->autoStyles as $element => $styles) {
186            $writerClass = 'PhpOffice\\PhpWord\\Writer\\ODText\\Style\\' . $element;
187            foreach ($styles as $style) {
188                /** @var \PhpOffice\PhpWord\Writer\ODText\Style\AbstractStyle $styleWriter Type hint */
189                $styleWriter = new $writerClass($xmlWriter, $style);
190                $styleWriter->write();
191            }
192        }
193
194        $xmlWriter->endElement(); // office:automatic-styles
195    }
196
197    /**
198     * Write automatic styles.
199     */
200    private function writeTextStyles(XMLWriter $xmlWriter): void
201    {
202        $styles = Style::getStyles();
203        $paragraphStyleCount = 0;
204
205        $style = new Paragraph();
206        $style->setStyleName('PB');
207        $style->setAuto();
208        $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
209        $styleWriter->write();
210
211        $sects = $this->getParentWriter()->getPhpWord()->getSections();
212        $countsects = count($sects);
213        for ($i = 0; $i < $countsects; ++$i) {
214            $iplus1 = $i + 1;
215            $style = new Paragraph();
216            $style->setStyleName("SB$iplus1");
217            $style->setAuto();
218            $pnstart = $sects[$i]->getStyle()->getPageNumberingStart();
219            $style->setNumLevel($pnstart);
220            $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
221            $styleWriter->write();
222        }
223
224        foreach ($styles as $style) {
225            $sty = (string) $style->getStyleName();
226            if (substr($sty, 0, 8) === 'Heading_') {
227                $style = new Paragraph();
228                $style->setStyleName('HD' . substr($sty, 8));
229                $style->setAuto();
230                $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
231                $styleWriter->write();
232                $style = new Paragraph();
233                $style->setStyleName('HE' . substr($sty, 8));
234                $style->setAuto();
235                $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
236                $styleWriter->write();
237            }
238        }
239
240        foreach ($styles as $style) {
241            if ($style->isAuto() === true) {
242                $styleClass = str_replace('\\Style\\', '\\Writer\\ODText\\Style\\', get_class($style));
243                if (class_exists($styleClass)) {
244                    /** @var \PhpOffice\PhpWord\Writer\ODText\Style\AbstractStyle $styleWriter Type hint */
245                    $styleWriter = new $styleClass($xmlWriter, $style);
246                    $styleWriter->write();
247                }
248                if ($style instanceof Paragraph) {
249                    ++$paragraphStyleCount;
250                }
251            }
252        }
253        foreach ($this->imageParagraphStyles as $style) {
254            $styleWriter = new ParagraphStyleWriter($xmlWriter, $style);
255            $styleWriter->write();
256        }
257    }
258
259    /**
260     * Get automatic styles.
261     */
262    private function getAutoStyles(PhpWord $phpWord): void
263    {
264        $sections = $phpWord->getSections();
265        $paragraphStyleCount = 0;
266        $fontStyleCount = 0;
267        foreach ($sections as $section) {
268            $style = $section->getStyle();
269            $style->setStyleName("Section{$section->getSectionId()}");
270            $this->autoStyles['Section'][] = $style;
271            $this->getContainerStyle($section, $paragraphStyleCount, $fontStyleCount);
272        }
273    }
274
275    /**
276     * Get all styles of each elements in container recursively.
277     *
278     * Table style can be null or string of the style name
279     *
280     * @param AbstractContainer $container
281     * @param int $paragraphStyleCount
282     * @param int $fontStyleCount
283     *
284     * @todo Simplify the logic
285     */
286    private function getContainerStyle($container, &$paragraphStyleCount, &$fontStyleCount): void
287    {
288        $elements = $container->getElements();
289        foreach ($elements as $element) {
290            if ($element instanceof TextRun) {
291                $this->getElementStyleTextRun($element, $paragraphStyleCount);
292                $this->getContainerStyle($element, $paragraphStyleCount, $fontStyleCount);
293            } elseif ($element instanceof Text) {
294                $this->getElementStyle($element, $paragraphStyleCount, $fontStyleCount);
295            } elseif ($element instanceof ListItem) {
296                $this->getElementStyle($element->getTextObject(), $paragraphStyleCount, $fontStyleCount);
297            } elseif ($element instanceof Field) {
298                $this->getElementStyleField($element, $fontStyleCount);
299            } elseif ($element instanceof Image) {
300                $style = $element->getStyle();
301                $style->setStyleName('fr' . $element->getMediaIndex());
302                $this->autoStyles['Image'][] = $style;
303                $sty = new Paragraph();
304                $sty->setStyleName('IM' . $element->getMediaIndex());
305                $sty->setAuto();
306                $sty->setAlignment($style->getAlignment());
307                $this->imageParagraphStyles[] = $sty;
308            } elseif ($element instanceof Shape) {
309                $style = $element->getStyle();
310                $style->setStyleName('sh' . $element->getElementId());
311                $this->autoStyles['Shape'][] = $style;
312            } elseif ($element instanceof TextBox) {
313                $style = $element->getStyle();
314                $style->setStyleName('tb' . $element->getElementId());
315                $this->autoStyles['TextBox'][] = $style;
316                $this->getContainerStyle($element, $paragraphStyleCount, $fontStyleCount);
317            } elseif ($element instanceof Line) {
318                if (!$element->getElementId()) {
319                    $element->setElementId();
320                }
321                $style = $element->getStyle() ?: new LineStyle();
322                $style->setStyleName($element->getElementId());
323                $this->autoStyles['Line'][] = $style;
324            } elseif ($element instanceof Table) {
325                $style = $element->getStyle();
326                if (is_string($style)) {
327                    $style = Style::getStyle($style);
328                }
329                if ($style === null) {
330                    $style = new TableStyle();
331                }
332                $style->setStyleName($element->getElementId());
333                $style->setColumnWidths($element->findFirstDefinedCellWidths());
334                $this->autoStyles['Table'][] = $style;
335                $rows = $element->getRows();
336                $rowCount = count($rows);
337                foreach ($rows as $rowIndex => $row) {
338                    if ($row instanceof RowElement && $row->getHeight() !== null) {
339                        if (!$row->getElementId()) {
340                            $row->setElementId();
341                        }
342                        $row->getStyle()->setStyleName($row->getElementId());
343                        $this->autoStyles['Row'][] = $row;
344                    }
345                    $cells = $row->getCells();
346                    $cellCount = count($cells);
347                    foreach ($cells as $cellIndex => $cell) {
348                        if ($cell instanceof CellElement) {
349                            if ($style instanceof TableStyle) {
350                                $this->applyTableStyleToCell($cell, $style, $rowIndex, $cellIndex, $rowCount, $cellCount);
351                            }
352                        }
353                        if ($cell instanceof CellElement && $this->hasCellStyle($cell)) {
354                            if (!$cell->getElementId()) {
355                                $cell->setElementId();
356                            }
357                            $cell->getStyle()->setStyleName($cell->getElementId());
358                            $this->autoStyles['Cell'][] = $cell->getStyle();
359                        }
360                        if ($cell instanceof CellElement) {
361                            $this->getContainerStyle($cell, $paragraphStyleCount, $fontStyleCount);
362                        }
363                    }
364                }
365            }
366        }
367    }
368
369    private function hasCellStyle(CellElement $cell): bool
370    {
371        $style = $cell->getStyle();
372
373        return $style !== null && (
374            $style->hasBorder()
375            || $style->getBgColor() !== null
376            || $style->getVAlign() !== null
377            || $style->getTextDirection() !== null
378            || $style->getPaddingTop() !== null
379            || $style->getPaddingBottom() !== null
380            || $style->getPaddingLeft() !== null
381            || $style->getPaddingRight() !== null
382            || $style->getNoWrap() === false
383        );
384    }
385
386    private function applyTableStyleToCell(CellElement $cell, TableStyle $tableStyle, int $rowIndex, int $cellIndex, int $rowCount, int $cellCount): void
387    {
388        $style = $cell->getStyle();
389        if ($style === null) {
390            return;
391        }
392
393        $this->setCellBorderIfUnset($style, 'Top', $rowIndex === 0 ? $tableStyle->getBorderTopSize() : $tableStyle->getBorderInsideHSize(), $rowIndex === 0 ? $tableStyle->getBorderTopColor() : $tableStyle->getBorderInsideHColor());
394        $this->setCellBorderIfUnset($style, 'Bottom', $rowIndex === $rowCount - 1 ? $tableStyle->getBorderBottomSize() : null, $tableStyle->getBorderBottomColor());
395        $this->setCellBorderIfUnset($style, 'Left', $cellIndex === 0 ? $tableStyle->getBorderLeftSize() : $tableStyle->getBorderInsideVSize(), $cellIndex === 0 ? $tableStyle->getBorderLeftColor() : $tableStyle->getBorderInsideVColor());
396        $this->setCellBorderIfUnset($style, 'Right', $cellIndex === $cellCount - 1 ? $tableStyle->getBorderRightSize() : null, $tableStyle->getBorderRightColor());
397
398        if ($style->getPaddingTop() === null && $tableStyle->getCellMarginTop() !== null) {
399            $style->setPaddingTop($tableStyle->getCellMarginTop());
400        }
401        if ($style->getPaddingBottom() === null && $tableStyle->getCellMarginBottom() !== null) {
402            $style->setPaddingBottom($tableStyle->getCellMarginBottom());
403        }
404        if ($style->getPaddingLeft() === null && $tableStyle->getCellMarginLeft() !== null) {
405            $style->setPaddingLeft($tableStyle->getCellMarginLeft());
406        }
407        if ($style->getPaddingRight() === null && $tableStyle->getCellMarginRight() !== null) {
408            $style->setPaddingRight($tableStyle->getCellMarginRight());
409        }
410    }
411
412    /**
413     * @param null|float|int $size
414     */
415    private function setCellBorderIfUnset(CellStyle $style, string $side, $size, ?string $color): void
416    {
417        $getSize = 'getBorder' . $side . 'Size';
418        if ($style->$getSize() !== null || $size === null) {
419            return;
420        }
421
422        $style->{'setBorder' . $side . 'Size'}($size);
423        $style->{'setBorder' . $side . 'Color'}($color);
424    }
425
426    /**
427     * Get style of individual element.
428     *
429     * @param Text $element
430     * @param int $paragraphStyleCount
431     * @param int $fontStyleCount
432     */
433    private function getElementStyle($element, &$paragraphStyleCount, &$fontStyleCount): void
434    {
435        $fontStyle = $element->getFontStyle();
436        $paragraphStyle = $element->getParagraphStyle();
437        $phpWord = $this->getParentWriter()->getPhpWord();
438
439        if ($fontStyle instanceof Font) {
440            // Font
441            $name = $fontStyle->getStyleName();
442            if (!$name) {
443                ++$fontStyleCount;
444                $style = $phpWord->addFontStyle("T{$fontStyleCount}", $fontStyle, null);
445                $style->setAuto();
446                $style->setParagraph(null);
447                $element->setFontStyle("T{$fontStyleCount}");
448            } else {
449                $element->setFontStyle($name);
450            }
451        }
452        if ($paragraphStyle instanceof Paragraph) {
453            // Paragraph
454            $name = $paragraphStyle->getStyleName();
455            if (!$name) {
456                ++$paragraphStyleCount;
457                $style = $phpWord->addParagraphStyle("P{$paragraphStyleCount}", $paragraphStyle);
458                $style->setAuto();
459                $element->setParagraphStyle("P{$paragraphStyleCount}");
460            } else {
461                $element->setParagraphStyle($name);
462            }
463        } elseif ($paragraphStyle) {
464            ++$paragraphStyleCount;
465            $parstylename = "P$paragraphStyleCount" . "_$paragraphStyle";
466            $style = $phpWord->addParagraphStyle($parstylename, $paragraphStyle);
467            $style->setAuto();
468            $element->setParagraphStyle($parstylename);
469        }
470    }
471
472    /**
473     * Get font style of individual field element.
474     *
475     * @param Field $element
476     * @param int $fontStyleCount
477     */
478    private function getElementStyleField($element, &$fontStyleCount): void
479    {
480        $fontStyle = $element->getFontStyle();
481        $phpWord = $this->getParentWriter()->getPhpWord();
482
483        if ($fontStyle instanceof Font) {
484            $name = $fontStyle->getStyleName();
485            if (!$name) {
486                ++$fontStyleCount;
487                $style = $phpWord->addFontStyle("T{$fontStyleCount}", $fontStyle, null);
488                $style->setAuto();
489                $style->setParagraph(null);
490                $element->setFontStyle("T{$fontStyleCount}");
491            } else {
492                $element->setFontStyle($name);
493            }
494        }
495    }
496
497    /**
498     * Get style of individual element.
499     *
500     * @param TextRun $element
501     * @param int $paragraphStyleCount
502     */
503    private function getElementStyleTextRun($element, &$paragraphStyleCount): void
504    {
505        $paragraphStyle = $element->getParagraphStyle();
506        $phpWord = $this->getParentWriter()->getPhpWord();
507
508        if ($paragraphStyle instanceof Paragraph) {
509            // Paragraph
510            $name = $paragraphStyle->getStyleName();
511            if (!$name) {
512                ++$paragraphStyleCount;
513                $style = $phpWord->addParagraphStyle("P{$paragraphStyleCount}", $paragraphStyle);
514                $style->setAuto();
515                $element->setParagraphStyle("P{$paragraphStyleCount}");
516            } else {
517                $element->setParagraphStyle($name);
518            }
519        } elseif ($paragraphStyle) {
520            ++$paragraphStyleCount;
521            $parstylename = "P$paragraphStyleCount" . "_$paragraphStyle";
522            $style = $phpWord->addParagraphStyle($parstylename, $paragraphStyle);
523            $style->setAuto();
524            $element->setParagraphStyle($parstylename);
525        }
526    }
527
528    /**
529     * Finds all tracked changes.
530     *
531     * @param \PhpOffice\PhpWord\Element\AbstractElement[] $trackedChanges
532     */
533    private function collectTrackedChanges(AbstractContainer $container, &$trackedChanges = []): void
534    {
535        $elements = $container->getElements();
536        foreach ($elements as $element) {
537            if ($element->getTrackChange() != null) {
538                $trackedChanges[] = $element;
539            }
540            if (is_callable([$element, 'getElements'])) {
541                $this->collectTrackedChanges($element, $trackedChanges);
542            }
543        }
544    }
545
546    /**
547     * Collect form controls from the document containers.
548     *
549     * @param array<int, CheckBox|FormField|SDTElement> $controls
550     */
551    private function collectFormControls(AbstractContainer $container, array &$controls): void
552    {
553        foreach ($container->getElements() as $element) {
554            if ($element instanceof CheckBox || $element instanceof FormField || $element instanceof SDTElement) {
555                if (!$element->getElementId()) {
556                    $element->setElementId();
557                }
558                $controls[] = $element;
559            }
560            if ($element instanceof AbstractContainer) {
561                $this->collectFormControls($element, $controls);
562            }
563        }
564    }
565
566    /**
567     * Write the document-level form controls referenced by draw:control.
568     *
569     * @param array<int, CheckBox|FormField|SDTElement> $controls
570     */
571    private function writeForms(XMLWriter $xmlWriter, array $controls): void
572    {
573        if ($controls === []) {
574            return;
575        }
576
577        $xmlWriter->startElement('office:forms');
578        $xmlWriter->startElement('form:form');
579        $xmlWriter->writeAttribute('form:name', 'Standard');
580
581        foreach ($controls as $control) {
582            if ($control instanceof SDTElement) {
583                $id = 'sdt-' . $control->getElementId();
584                $type = $control->getType();
585                $controlType = $type === 'plainText' ? 'text' : ($type === 'dropDownList' ? 'listbox' : strtolower($type));
586                $xmlWriter->startElement('form:' . $controlType);
587                $xmlWriter->writeAttribute('xml:id', $id);
588                $xmlWriter->writeAttribute('form:name', $control->getAlias() ?: $id);
589                $value = $control->getValue();
590
591                if ($type === 'plainText' || $type === 'date') {
592                    $xmlWriter->writeAttributeIf($value !== null, 'form:value', (string) $value);
593                } elseif ($type === 'comboBox') {
594                    $xmlWriter->writeAttributeIf($value !== null, 'form:value', (string) $value);
595                    foreach ($control->getListItems() as $item) {
596                        $xmlWriter->startElement('form:item');
597                        $xmlWriter->writeAttribute('form:label', (string) $item);
598                        $xmlWriter->text((string) $item);
599                        $xmlWriter->endElement();
600                    }
601                } elseif ($type === 'dropDownList') {
602                    foreach ($control->getListItems() as $key => $item) {
603                        $xmlWriter->startElement('form:option');
604                        $xmlWriter->writeAttribute('form:value', (string) $key);
605                        $xmlWriter->writeAttribute('form:label', (string) $item);
606                        $xmlWriter->writeAttributeIf((string) $value === (string) $item, 'form:current-selected', 'true');
607                        $xmlWriter->text((string) $item);
608                        $xmlWriter->endElement();
609                    }
610                }
611
612                $xmlWriter->endElement();
613
614                continue;
615            }
616            $id = 'control-' . $control->getElementId();
617            $name = $control->getName();
618            $name = $name ?: (($control instanceof CheckBox ? 'checkbox' : $control->getType()) . $id);
619
620            if ($control instanceof CheckBox) {
621                $xmlWriter->startElement('form:checkbox');
622                $xmlWriter->writeAttribute('xml:id', $id);
623                $xmlWriter->writeAttribute('form:name', $name);
624                $xmlWriter->writeAttribute('form:label', (string) $control->getText());
625                $xmlWriter->writeAttribute('form:current-state', 'unchecked');
626                $xmlWriter->endElement();
627
628                continue;
629            }
630
631            switch ($control->getType()) {
632                case 'textinput':
633                    $xmlWriter->startElement('form:text');
634                    $xmlWriter->writeAttribute('xml:id', $id);
635                    $xmlWriter->writeAttribute('form:name', $name);
636                    if ($control->getDefault() !== null) {
637                        $xmlWriter->writeAttribute('form:value', (string) $control->getDefault());
638                    }
639                    if ($control->getValue() !== null) {
640                        $xmlWriter->writeAttribute('form:current-value', (string) $control->getValue());
641                    }
642                    $xmlWriter->endElement();
643
644                    break;
645                case 'checkbox':
646                    $xmlWriter->startElement('form:checkbox');
647                    $xmlWriter->writeAttribute('xml:id', $id);
648                    $xmlWriter->writeAttribute('form:name', $name);
649                    $state = $control->getValue() ?? $control->getDefault();
650                    $xmlWriter->writeAttribute('form:current-state', $state ? 'checked' : 'unchecked');
651                    $xmlWriter->endElement();
652
653                    break;
654                case 'dropdown':
655                    $xmlWriter->startElement('form:listbox');
656                    $xmlWriter->writeAttribute('xml:id', $id);
657                    $xmlWriter->writeAttribute('form:name', $name);
658                    $selected = $control->getValue() ?? $control->getDefault();
659                    foreach ($control->getEntries() as $index => $entry) {
660                        $xmlWriter->startElement('form:option');
661                        $xmlWriter->writeAttribute('form:label', (string) $entry);
662                        $xmlWriter->writeAttribute('form:value', (string) $entry);
663                        $xmlWriter->writeAttributeIf((int) $selected === (int) $index, 'form:current-selected', 'true');
664                        $xmlWriter->text((string) $entry);
665                        $xmlWriter->endElement();
666                    }
667                    $xmlWriter->endElement();
668
669                    break;
670            }
671        }
672
673        $xmlWriter->endElement(); // form:form
674        $xmlWriter->endElement(); // office:forms
675    }
676}