1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-stdlib for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-stdlib/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-stdlib/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Stdlib\StringWrapper;
10
11use Laminas\Stdlib\Exception;
12use Laminas\Stdlib\StringUtils;
13
14abstract class AbstractStringWrapper implements StringWrapperInterface
15{
16    /**
17     * The character encoding working on
18     * @var string|null
19     */
20    protected $encoding = 'UTF-8';
21
22    /**
23     * An optionally character encoding to convert to
24     * @var string|null
25     */
26    protected $convertEncoding;
27
28    /**
29     * Check if the given character encoding is supported by this wrapper
30     * and the character encoding to convert to is also supported.
31     *
32     * @param  string      $encoding
33     * @param  string|null $convertEncoding
34     * @return bool
35     */
36    public static function isSupported($encoding, $convertEncoding = null)
37    {
38        $supportedEncodings = static::getSupportedEncodings();
39
40        if (! in_array(strtoupper($encoding), $supportedEncodings)) {
41            return false;
42        }
43
44        if ($convertEncoding !== null && ! in_array(strtoupper($convertEncoding), $supportedEncodings)) {
45            return false;
46        }
47
48        return true;
49    }
50
51    /**
52     * Set character encoding working with and convert to
53     *
54     * @param string      $encoding         The character encoding to work with
55     * @param string|null $convertEncoding  The character encoding to convert to
56     * @return StringWrapperInterface
57     */
58    public function setEncoding($encoding, $convertEncoding = null)
59    {
60        $supportedEncodings = static::getSupportedEncodings();
61
62        $encodingUpper = strtoupper($encoding);
63        if (! in_array($encodingUpper, $supportedEncodings)) {
64            throw new Exception\InvalidArgumentException(
65                'Wrapper doesn\'t support character encoding "' . $encoding . '"'
66            );
67        }
68
69        if ($convertEncoding !== null) {
70            $convertEncodingUpper = strtoupper($convertEncoding);
71            if (! in_array($convertEncodingUpper, $supportedEncodings)) {
72                throw new Exception\InvalidArgumentException(
73                    'Wrapper doesn\'t support character encoding "' . $convertEncoding . '"'
74                );
75            }
76
77            $this->convertEncoding = $convertEncodingUpper;
78        } else {
79            $this->convertEncoding = null;
80        }
81        $this->encoding = $encodingUpper;
82
83        return $this;
84    }
85
86    /**
87     * Get the defined character encoding to work with
88     *
89     * @return string
90     * @throws Exception\LogicException If no encoding was defined
91     */
92    public function getEncoding()
93    {
94        return $this->encoding;
95    }
96
97    /**
98     * Get the defined character encoding to convert to
99     *
100     * @return string|null
101    */
102    public function getConvertEncoding()
103    {
104        return $this->convertEncoding;
105    }
106
107    /**
108     * Convert a string from defined character encoding to the defined convert encoding
109     *
110     * @param string  $str
111     * @param bool $reverse
112     * @return string|false
113     */
114    public function convert($str, $reverse = false)
115    {
116        $encoding        = $this->getEncoding();
117        $convertEncoding = $this->getConvertEncoding();
118        if ($convertEncoding === null) {
119            throw new Exception\LogicException(
120                'No convert encoding defined'
121            );
122        }
123
124        if ($encoding === $convertEncoding) {
125            return $str;
126        }
127
128        $from = $reverse ? $convertEncoding : $encoding;
129        $to   = $reverse ? $encoding : $convertEncoding;
130        throw new Exception\RuntimeException(sprintf(
131            'Converting from "%s" to "%s" isn\'t supported by this string wrapper',
132            $from,
133            $to
134        ));
135    }
136
137    /**
138     * Wraps a string to a given number of characters
139     *
140     * @param  string  $string
141     * @param  int $width
142     * @param  string  $break
143     * @param  bool $cut
144     * @return string|false
145     */
146    public function wordWrap($string, $width = 75, $break = "\n", $cut = false)
147    {
148        $string = (string) $string;
149        if ($string === '') {
150            return '';
151        }
152
153        $break = (string) $break;
154        if ($break === '') {
155            throw new Exception\InvalidArgumentException('Break string cannot be empty');
156        }
157
158        $width = (int) $width;
159        if ($width === 0 && $cut) {
160            throw new Exception\InvalidArgumentException('Cannot force cut when width is zero');
161        }
162
163        if (StringUtils::isSingleByteEncoding($this->getEncoding())) {
164            return wordwrap($string, $width, $break, $cut);
165        }
166
167        $stringWidth = $this->strlen($string);
168        $breakWidth  = $this->strlen($break);
169
170        $result    = '';
171        $lastStart = $lastSpace = 0;
172
173        for ($current = 0; $current < $stringWidth; $current++) {
174            $char = $this->substr($string, $current, 1);
175
176            $possibleBreak = $char;
177            if ($breakWidth !== 1) {
178                $possibleBreak = $this->substr($string, $current, $breakWidth);
179            }
180
181            if ($possibleBreak === $break) {
182                $result    .= $this->substr($string, $lastStart, $current - $lastStart + $breakWidth);
183                $current   += $breakWidth - 1;
184                $lastStart  = $lastSpace = $current + 1;
185                continue;
186            }
187
188            if ($char === ' ') {
189                if ($current - $lastStart >= $width) {
190                    $result    .= $this->substr($string, $lastStart, $current - $lastStart) . $break;
191                    $lastStart  = $current + 1;
192                }
193
194                $lastSpace = $current;
195                continue;
196            }
197
198            if ($current - $lastStart >= $width && $cut && $lastStart >= $lastSpace) {
199                $result    .= $this->substr($string, $lastStart, $current - $lastStart) . $break;
200                $lastStart  = $lastSpace = $current;
201                continue;
202            }
203
204            if ($current - $lastStart >= $width && $lastStart < $lastSpace) {
205                $result    .= $this->substr($string, $lastStart, $lastSpace - $lastStart) . $break;
206                $lastStart  = $lastSpace = $lastSpace + 1;
207                continue;
208            }
209        }
210
211        if ($lastStart !== $current) {
212            $result .= $this->substr($string, $lastStart, $current - $lastStart);
213        }
214
215        return $result;
216    }
217
218    /**
219     * Pad a string to a certain length with another string
220     *
221     * @param  string  $input
222     * @param  int $padLength
223     * @param  string  $padString
224     * @param  int $padType
225     * @return string
226     */
227    public function strPad($input, $padLength, $padString = ' ', $padType = STR_PAD_RIGHT)
228    {
229        if (StringUtils::isSingleByteEncoding($this->getEncoding())) {
230            return str_pad($input, $padLength, $padString, $padType);
231        }
232
233        $lengthOfPadding = $padLength - $this->strlen($input);
234        if ($lengthOfPadding <= 0) {
235            return $input;
236        }
237
238        $padStringLength = $this->strlen($padString);
239        if ($padStringLength === 0) {
240            return $input;
241        }
242
243        $repeatCount = floor($lengthOfPadding / $padStringLength);
244
245        if ($padType === STR_PAD_BOTH) {
246            $repeatCountLeft = $repeatCountRight = ($repeatCount - $repeatCount % 2) / 2;
247
248            $lastStringLength       = $lengthOfPadding - 2 * $repeatCountLeft * $padStringLength;
249            $lastStringLeftLength   = $lastStringRightLength = floor($lastStringLength / 2);
250            $lastStringRightLength += $lastStringLength % 2;
251
252            $lastStringLeft  = $this->substr($padString, 0, $lastStringLeftLength);
253            $lastStringRight = $this->substr($padString, 0, $lastStringRightLength);
254
255            return str_repeat($padString, $repeatCountLeft) . $lastStringLeft
256                . $input
257                . str_repeat($padString, $repeatCountRight) . $lastStringRight;
258        }
259
260        $lastString = $this->substr($padString, 0, $lengthOfPadding % $padStringLength);
261
262        if ($padType === STR_PAD_LEFT) {
263            return str_repeat($padString, $repeatCount) . $lastString . $input;
264        }
265
266        return $input . str_repeat($padString, $repeatCount) . $lastString;
267    }
268}
269