1<?php
2
3namespace dokuwiki\plugin\wordimport\docx;
4
5/**
6 * A paragraph
7 *
8 * This the most common but also most complex type. It can contain multiple text runs each with different formatting
9 */
10class Paragraph extends AbstractParagraph
11{
12    /** @var TextRun[] */
13    protected $texts = [];
14
15    /** @var string */
16    protected $alignment = '';
17
18    /** @var array DokuWiki Syntax for the formatting */
19    protected $fSyntax = [
20        'bold' => ['**', '**'],
21        'italic' => ['//', '//'],
22        'underline' => ['__', '__'],
23        'strike' => ['<del>', '</del>'],
24        'mono' => ["''", "''"],
25    ];
26
27    /** @inheritdoc */
28    public function parse()
29    {
30        $trs = $this->p->xpath('w:r');
31        foreach ($trs as $tr) {
32            $textRun = new TextRun($this->docx, $tr);
33            $this->texts[] = $textRun;
34        }
35        $this->updateFormattingScores();
36
37        $alignment = $this->p->xpath('w:pPr/w:jc');
38        if ($alignment) {
39            $this->alignment = (string)$alignment[0]->attributes('w', true)->val;
40        }
41    }
42
43    /**
44     * This combines the texts of all runs and wraps them in the correct formatting. It tries to create the most
45     * optimal wrapping by closing and opening formatting as needed.
46     * @inheritdoc
47     */
48    public function __toString(): string
49    {
50        $result = '';
51        $fStack = [];
52
53        foreach ($this->texts as $text) {
54            // we don't want to wrap whitespace in formatting
55            if ($text->isWhiteSpace()) {
56                $result .= $text->__toString();
57                continue;
58            }
59
60            $formatting = $text->getFormatting();
61            $formatting = array_keys(array_filter($formatting));
62
63            // close all formatting that is not in the current text
64            $toclose = array_diff($fStack, $formatting);
65            foreach ($toclose as $f) {
66                // we need to make sure all formatting is closed, but we close by popping the
67                // stack. This ensures we don't create invalid nesting
68                while (in_array($f, $fStack)) {
69                    $this->closeFormatting($result, array_pop($fStack));
70                }
71            }
72
73            // open formatting that is in the current text
74            $new = array_diff($formatting, $fStack);
75            foreach ($new as $f) {
76                $this->openFormatting($result, $f);
77                $fStack[] = $f;
78            }
79
80            // add the text
81            $result .= $text->__toString();
82        }
83
84        // close remaining formatting
85        while ($fStack) {
86            $this->closeFormatting($result, array_pop($fStack));
87        }
88
89        return $result;
90    }
91
92    /**
93     * Update the formatting scores for all texts
94     *
95     * Walks through the texts in reverse order and updates the formatting scores
96     */
97    protected function updateFormattingScores()
98    {
99        $len = count($this->texts);
100        if ($len < 2) return;
101        for ($i = $len - 2; $i >= 0; $i--) {
102            $this->texts[$i]->updateFormattingScores($this->texts[$i + 1]);
103        }
104    }
105
106    /**
107     * Get the paragraph alignment
108     *
109     * This is only used in images and tables
110     *
111     * @return string
112     */
113    public function getAlignment()
114    {
115        return $this->alignment;
116    }
117
118    /**
119     * Pad the given text according to the alignment
120     *
121     * @param string $text
122     */
123    public function alignmentPadding($text)
124    {
125        switch ($this->getAlignment()) {
126            case 'left':
127                $text = "$text ";
128                break;
129            case 'right':
130                $text = " $text";
131                break;
132            case 'center':
133                $text = " $text ";
134                break;
135        }
136        return $text;
137    }
138
139    /**
140     * Open a formatting
141     *
142     * @param string $text The text currently worked on
143     * @param string $formatting Name of the formatting
144     * @return void
145     */
146    public function openFormatting(&$text, $formatting)
147    {
148        if (!isset($this->fSyntax[$formatting])) {
149            throw new \RuntimeException("Unknown formatting: $formatting");
150        }
151
152        $text .= $this->fSyntax[$formatting][0];
153    }
154
155    /**
156     * Close a formatting
157     *
158     * Handles whitespace at the end of the text
159     *
160     * @param string $text The text currently worked on
161     * @param string $formatting Name of the formatting
162     * @return void
163     */
164    public function closeFormatting(&$text, $formatting)
165    {
166        preg_match('/^(.+?)(\s*)$/s', $text, $matches);
167        $text = $matches[1];
168        $suffix = $matches[2];
169
170        if (!isset($this->fSyntax[$formatting])) {
171            throw new \RuntimeException("Unknown formatting: $formatting");
172        }
173
174        $text .= $this->fSyntax[$formatting][1];
175        $text .= $suffix;
176    }
177}
178