Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.61% covered (success)
98.61%
71 / 72
88.89% covered (warning)
88.89%
8 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Contents
98.61% covered (success)
98.61%
71 / 72
88.89% covered (warning)
88.89%
8 / 9
28
0.00% covered (danger)
0.00%
0 / 1
 write
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 writeChunks
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
2
 writeFontNames
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 writeContainer
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 writeTable
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 writeParagraph
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 writeInline
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 getElementText
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 sanitizeText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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\WPS\Part;
20
21use PhpOffice\PhpWord\Element\AbstractContainer;
22use PhpOffice\PhpWord\Element\AbstractElement;
23use PhpOffice\PhpWord\Element\Link;
24use PhpOffice\PhpWord\Element\ListItem;
25use PhpOffice\PhpWord\Element\PageBreak;
26use PhpOffice\PhpWord\Element\Table;
27use PhpOffice\PhpWord\Element\Text;
28use PhpOffice\PhpWord\Element\TextBreak;
29use PhpOffice\PhpWord\Element\Title;
30
31/**
32 * WPS CONTENTS part writer.
33 *
34 * The stream is a list of chunks, each of them described by an entry of the
35 * index which follows the header of the stream.
36 *
37 * @since 1.5.0
38 */
39class Contents extends AbstractPart
40{
41    /** Magic of the Microsoft Works 7/8 word processor documents. */
42    const MAGIC = "CHNKWKS\x00";
43
44    /** Size of the stream header, including the header of the index. */
45    const HEADER_SIZE = 0x20;
46
47    /** Size of an entry of the index. */
48    const ENTRY_SIZE = 0x18;
49
50    /** End of paragraph. */
51    const PARAGRAPH_END = "\x0d";
52
53    /** Page break. */
54    const PAGE_BREAK = "\x0c";
55
56    /**
57     * Write the CONTENTS stream.
58     */
59    public function write(): string
60    {
61        $phpWord = $this->getParentWriter()->getPhpWord();
62
63        $text = '';
64        foreach ($phpWord->getSections() as $section) {
65            $text .= $this->writeContainer($section);
66        }
67
68        return $this->writeChunks([
69            'TEXT' => $this->encodeText($text),
70            'FONT' => $this->writeFontNames([$phpWord->getDefaultFontName()]),
71        ]);
72    }
73
74    /**
75     * Write the header, the index and the data of the chunks.
76     *
77     * @param array<string, string> $chunks Chunk name and content pairs
78     */
79    private function writeChunks(array $chunks): string
80    {
81        $offset = self::HEADER_SIZE + count($chunks) * self::ENTRY_SIZE;
82
83        $index = '';
84        $data = '';
85        $id = 0;
86        foreach ($chunks as $name => $content) {
87            $index .= pack('v', self::ENTRY_SIZE);
88            $index .= $name; // name of the chunk
89            $index .= pack('vvv', $id++, 0, 0);
90            $index .= $name; // type of the chunk
91            $index .= pack('VV', $offset + strlen($data), strlen($content));
92            $data .= $content;
93        }
94
95        $header = self::MAGIC;
96        $header .= pack('vvv', 0, 0, count($chunks)); // unknown, unknown, number of chunks
97        $header .= str_repeat("\x00", 10); // unknown
98        $header .= pack('vv', 0, count($chunks)); // unknown, number of chunks of this index
99        $header .= pack('V', 0xFFFFFFFF); // offset of the next index, none
100
101        return $header . $index . $data;
102    }
103
104    /**
105     * Write the table of the font names.
106     *
107     * @param string[] $fonts
108     */
109    private function writeFontNames(array $fonts): string
110    {
111        $names = '';
112        foreach ($fonts as $font) {
113            $names .= pack('v', mb_strlen($font));
114            $names .= $this->encodeText($font);
115            $names .= str_repeat("\x00", 4); // unknown
116        }
117
118        $offsets = str_repeat(pack('V', 0), count($fonts));
119
120        return pack('V', strlen($offsets) + strlen($names)) // size of the chunk, without this header
121            . pack('V', count($fonts))
122            . str_repeat("\x00", 12) // unknown
123            . $offsets
124            . $names;
125    }
126
127    /**
128     * Write the paragraphs of a container.
129     */
130    private function writeContainer(AbstractContainer $container): string
131    {
132        $text = '';
133        foreach ($container->getElements() as $element) {
134            if ($element instanceof PageBreak) {
135                $text .= self::PAGE_BREAK;
136            } elseif ($element instanceof TextBreak) {
137                $text .= self::PARAGRAPH_END;
138            } elseif ($element instanceof Table) {
139                $text .= $this->writeTable($element);
140            } elseif ($element instanceof AbstractContainer) {
141                $text .= $this->writeParagraph($this->writeInline($element));
142            } else {
143                $text .= $this->writeParagraph($this->getElementText($element));
144            }
145        }
146
147        return $text;
148    }
149
150    /**
151     * Write the paragraphs of the cells of a table.
152     */
153    private function writeTable(Table $table): string
154    {
155        $text = '';
156        foreach ($table->getRows() as $row) {
157            foreach ($row->getCells() as $cell) {
158                $text .= $this->writeContainer($cell);
159            }
160        }
161
162        return $text;
163    }
164
165    /**
166     * Write a paragraph, ie its text followed by an end of paragraph.
167     */
168    private function writeParagraph(string $text): string
169    {
170        if ($text === '') {
171            return '';
172        }
173
174        return $text . self::PARAGRAPH_END;
175    }
176
177    /**
178     * Get the text of the elements of a container, without paragraph separator.
179     */
180    private function writeInline(AbstractContainer $container): string
181    {
182        $text = '';
183        foreach ($container->getElements() as $element) {
184            if ($element instanceof AbstractContainer) {
185                $text .= $this->writeInline($element);
186            } else {
187                $text .= $this->getElementText($element);
188            }
189        }
190
191        return $text;
192    }
193
194    /**
195     * Get the text of an element, empty for the unsupported ones.
196     */
197    private function getElementText(AbstractElement $element): string
198    {
199        if ($element instanceof Text || $element instanceof ListItem) {
200            $text = $element->getText();
201        } elseif ($element instanceof Link) {
202            $text = $element->getText() !== '' ? $element->getText() : $element->getSource();
203        } elseif ($element instanceof Title) {
204            $title = $element->getText();
205            $text = $title instanceof AbstractContainer ? $this->writeInline($title) : $title;
206        } else {
207            $text = '';
208        }
209
210        return $this->sanitizeText((string) $text);
211    }
212
213    /**
214     * Replace the characters which delimit the paragraphs by spaces.
215     */
216    private function sanitizeText(string $text): string
217    {
218        return str_replace(["\r\n", "\r", "\n", self::PAGE_BREAK], ' ', $text);
219    }
220}