1<?php
2/**
3 * Copyright (c) 2020. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15use IntlDateFormatter;
16
17/**
18 * Class PipelineUtility
19 * @package ComboStrap
20 * A pipeline to perform filter transformation
21 *
22 * See also
23 * https://getbootstrap.com/docs/5.0/helpers/text-truncation/
24 */
25class PipelineUtility
26{
27    const QUOTES_CHARACTERS = ['"', '\''];
28    const SPACE_CHARACTER = " ";
29
30
31    /**
32     * @param $expression
33     * @param array|null $contextData
34     * @return string
35     * @throws ExceptionBadSyntax - if there is any syntax error
36     */
37    static public function execute($expression, array $contextData = null): string
38    {
39
40        /**
41         * Get the value (called the message in a pipeline)
42         */
43        $processedExpression = $expression;
44        $firstQuoteChar = strpos($processedExpression, '"');
45        $firstPipeChar = strpos($processedExpression, '|');
46        if ($firstQuoteChar < $firstPipeChar || $firstPipeChar === false) {
47
48            /**
49             * Example:
50             * a literal: "$title"
51             * a literal with a pipe: "World | Do" | replace ("world,"you")
52             */
53            $message = null;
54            if ($firstQuoteChar !== false) {
55                $processedExpression = substr($processedExpression, $firstQuoteChar + 1);
56                $secondQuoteChar = strpos($processedExpression, '"');
57                if ($secondQuoteChar !== false) {
58                    $message = substr($processedExpression, 0, $secondQuoteChar);
59                }
60            }
61
62            $pipeCharPosition = strpos($processedExpression, '|');
63            if ($pipeCharPosition !== false) {
64                $commandChain = substr($processedExpression, $pipeCharPosition + 1);
65                if ($message == null) {
66                    // not quoted expression
67                    // do we support that ?
68                    $message = substr($processedExpression, 0, $pipeCharPosition);
69                }
70            } else {
71                if ($message == null) {
72                    // not quoted expression
73                    // do we support that ?
74                    $message = $processedExpression;
75                }
76                $commandChain = "";
77            }
78
79        } else {
80
81            /**
82             * Example: a variable with an expression
83             * $title | replace ("world,"you")
84             */
85            $message = trim(substr($processedExpression, 0, $firstPipeChar));
86            $commandChain = trim(substr($processedExpression, $firstPipeChar + 1));
87
88        }
89
90
91        /**
92         * Command chain splits
93         */
94        $commands = preg_split("/\|/", $commandChain);
95
96
97        /**
98         * We replace after the split to be sure that there is not a | separator in the variable value
99         * that would fuck up the process
100         */
101        $message = \syntax_plugin_combo_variable::replaceVariablesWithValuesFromContext($message, $contextData);
102
103        $charactersToTrimFromCommand = implode("", self::QUOTES_CHARACTERS);
104        foreach ($commands as $command) {
105            $command = trim($command, " )");
106            $leftParenthesis = strpos($command, "(");
107            $commandName = substr($command, 0, $leftParenthesis);
108            $signature = substr($command, $leftParenthesis + 1);
109            $commandArgs = preg_split("/\s*,\s*/", $signature);
110            /**
111             * Delete space characters
112             */
113            $commandArgs = array_map(
114                'trim',
115                $commandArgs,
116                array_fill(0, sizeof($commandArgs), self::SPACE_CHARACTER)
117            );
118            /**
119             * Delete quote characters
120             */
121            $commandArgs = array_map(
122                'trim',
123                $commandArgs,
124                array_fill(0, sizeof($commandArgs), $charactersToTrimFromCommand)
125            );
126            $commandName = trim($commandName);
127            if (!empty($commandName)) {
128                switch ($commandName) {
129                    case "replace":
130                        $message = self::replace($commandArgs, $message);
131                        break;
132                    case "head":
133                        $message = self::head($commandArgs, $message);
134                        break;
135                    case "tail":
136                        $message = self::tail($commandArgs, $message);
137                        break;
138                    case "rconcat":
139                        $message = self::concat($commandArgs, $message, "right");
140                        break;
141                    case "lconcat":
142                        $message = self::concat($commandArgs, $message, "left");
143                        break;
144                    case "cut":
145                        $message = self::cut($commandArgs, $message);
146                        break;
147                    case "trim":
148                        $message = trim($message);
149                        break;
150                    case "capitalize":
151                        $message = ucwords($message);
152                        break;
153                    case "format":
154                        $message = self::format($commandArgs, $message);
155                        break;
156                    default:
157                        LogUtility::msg("command ($commandName) is unknown", LogUtility::LVL_MSG_ERROR, "pipeline");
158                }
159            }
160        }
161        return $message;
162    }
163
164    private static function replace(array $commandArgs, $value)
165    {
166        $search = $commandArgs[0];
167        $replace = $commandArgs[1];
168        return str_replace($search, $replace, $value);
169    }
170
171    /**
172     * @param array $commandArgs
173     * @param $value
174     * @return false|string
175     * See also: https://getbootstrap.com/docs/5.0/helpers/text-truncation/
176     */
177    public static function head(array $commandArgs, $value)
178    {
179        $length = $commandArgs[0];
180        if (strlen($value) < $length) {
181            return $value;
182        }
183        $words = explode(" ", $value);
184        $headValue = "";
185        for ($i = 0; $i < sizeof($words); $i++) {
186            if ($i != 0) {
187                $headValue .= " ";
188            }
189            $headValue .= $words[$i];
190            if (strlen($headValue) >= $length) {
191                break;
192            }
193        }
194
195        $tail = $commandArgs[1] ?? null;
196        if ($tail !== null) {
197            $headValue .= $tail;
198        }
199
200        return $headValue;
201    }
202
203    private
204    static function concat(array $commandArgs, $value, $side): string
205    {
206        $string = $commandArgs[0];
207        switch ($side) {
208            case "left":
209                return $string . $value;
210            case "right":
211                return $value . $string;
212            default:
213                LogUtility::msg("The side value ($side) is unknown", LogUtility::LVL_MSG_ERROR, "pipeline");
214                return $value . $string;
215        }
216
217
218    }
219
220    private
221    static function tail(array $commandArgs, $value)
222    {
223        $length = $commandArgs[0];
224        return substr($value, strlen($value) - $length);
225    }
226
227    private
228    static function cut(array $commandArgs, $value)
229    {
230        $pattern = $commandArgs[0];
231        $words = preg_split("/$pattern/i", $value);
232        if ($words !== false) {
233            $selector = $commandArgs[1];
234            $startEndSelector = preg_split("/-/i", $selector);
235            $start = $startEndSelector[0] - 1;
236            $end = null;
237            if (isset($startEndSelector[1])) {
238                $end = $startEndSelector[1];
239                if (empty($end)) {
240                    $end = sizeof($words);
241                }
242                $end = $end - 1;
243            }
244            if ($end == null) {
245                if (isset($words[$start])) {
246                    return $words[$start];
247                } else {
248                    return $value;
249                }
250            } else {
251                $result = "";
252                for ($i = $start; $i <= $end; $i++) {
253                    if (isset($words[$i])) {
254                        if (!empty($result)) {
255                            $result .= $pattern;
256                        }
257                        $result .= $words[$i];
258                    }
259                }
260                return $result;
261            }
262
263        } else {
264            return "An error occurred: could not split with the pattern `$pattern`, the value `$value`.";
265        }
266    }
267
268    /**
269     * @throws ExceptionBadSyntax
270     */
271    public
272    static function format(array $commandArgs, $value): string
273    {
274
275        /**
276         * For now only date time are
277         */
278        try {
279            $dateTime = Iso8601Date::createFromString($value);
280        } catch (ExceptionBadSyntax $e) {
281            throw new ExceptionBadSyntax("The format method allows for now only date. The value ($value) is not a date.", PipelineTag::CANONICAL);
282        }
283
284        $size = sizeof($commandArgs);
285        $pattern = null;
286        $locale = null;
287        switch ($size) {
288            case 0:
289                break;
290            case 1:
291                $pattern = $commandArgs[0];
292                break;
293            case 2:
294            default:
295                $pattern = $commandArgs[0];
296                $locale = $commandArgs[1];
297                break;
298        }
299        $localeSeparator = '_';
300        if ($locale === null) {
301            $path = ExecutionContext::getActualOrCreateFromEnv()->getContextPath();
302            $page = MarkupPath::createPageFromPathObject($path);
303            $locale = Locale::createForPage($page)->getValueOrDefault();
304        }
305
306        if ($locale === null) {
307            // should never happen but yeah
308            $locale = 'en_US';
309            LogUtility::error("Internal Error: No default locale could be determined. The locale was set to $locale", DateTag::CANONICAL);
310        }
311
312        /**
313         * If the user has set a lang
314         * Transform it as locale
315         */
316        if (strlen(trim($locale)) === 2) {
317            $derivedLocale = strtolower($locale) . $localeSeparator . strtoupper($locale);
318        } else {
319            $derivedLocale = $locale;
320        }
321
322        return $dateTime->formatLocale($pattern, $derivedLocale);
323
324    }
325
326}
327