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