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