1<?php
2
3/*
4 * This file is part of the Fxp Composer Asset Plugin package.
5 *
6 * (c) François Pluchino <francois.pluchino@gmail.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12namespace Fxp\Composer\AssetPlugin\Converter;
13
14/**
15 * Converter for Semver syntax version to composer syntax version.
16 *
17 * @author François Pluchino <francois.pluchino@gmail.com>
18 */
19class SemverConverter implements VersionConverterInterface
20{
21    /**
22     * {@inheritdoc}
23     */
24    public function convertVersion($version)
25    {
26        if (\in_array($version, array(null, '', 'latest'), true)) {
27            return ('latest' === $version ? 'default || ' : '').'*';
28        }
29
30        $version = str_replace('–', '-', $version);
31        $prefix = preg_match('/^[a-z]/', $version) && 0 !== strpos($version, 'dev-') ? substr($version, 0, 1) : '';
32        $version = substr($version, \strlen($prefix));
33        $version = SemverUtil::convertVersionMetadata($version);
34        $version = SemverUtil::convertDateVersion($version);
35
36        return $prefix.$version;
37    }
38
39    /**
40     * {@inheritdoc}
41     */
42    public function convertRange($range)
43    {
44        $range = $this->cleanRange(strtolower($range));
45
46        return $this->matchRange($range);
47    }
48
49    /**
50     * Clean the raw range.
51     *
52     * @param string $range
53     *
54     * @return string
55     */
56    protected function cleanRange($range)
57    {
58        foreach (array('<', '>', '=', '~', '^', '||', '&&') as $character) {
59            $range = str_replace($character.' ', $character, $range);
60        }
61
62        $range = preg_replace('/(?:[vV])(\d+)/', '${1}', $range);
63        $range = str_replace(' ||', '||', $range);
64
65        return str_replace(array(' &&', '&&'), ',', $range);
66    }
67
68    /**
69     * Match the range.
70     *
71     * @param string $range The range cleaned
72     *
73     * @return string The range
74     */
75    protected function matchRange($range)
76    {
77        $pattern = '/(\ -\ )|(<)|(>)|(=)|(\|\|)|(\ )|(,)|(\~)|(\^)/';
78        $matches = preg_split($pattern, $range, -1, PREG_SPLIT_DELIM_CAPTURE);
79        $special = null;
80        $replace = null;
81        $first = true;
82
83        foreach ($matches as $i => $match) {
84            if ($first && '' !== $match) {
85                $first = false;
86                $match = '=' === $match ? 'EQUAL' : $match;
87            }
88
89            $this->matchRangeToken($i, $match, $matches, $special, $replace);
90        }
91
92        return implode('', $matches);
93    }
94
95    /**
96     * Converts the token of the matched range.
97     *
98     * @param int         $i
99     * @param string      $match
100     * @param array       $matches
101     * @param null|string $special
102     * @param null|string $replace
103     */
104    protected function matchRangeToken($i, $match, array &$matches, &$special, &$replace)
105    {
106        if (' - ' === $match) {
107            $matches[$i - 1] = '>='.str_replace(array('*', 'x', 'X'), '0', $matches[$i - 1]);
108
109            if (false !== strpos($matches[$i + 1], '.') && false === strpos($matches[$i + 1], '*')
110                    && false === strpos($matches[$i + 1], 'x') && false === strpos($matches[$i + 1], 'X')) {
111                $matches[$i] = ',<=';
112            } else {
113                $matches[$i] = ',<';
114                $special = ',<~';
115            }
116        } else {
117            $this->matchRangeTokenStep2($i, $match, $matches, $special, $replace);
118        }
119    }
120
121    /**
122     * Step2: Converts the token of the matched range.
123     *
124     * @param int         $i
125     * @param string      $match
126     * @param array       $matches
127     * @param null|string $special
128     * @param null|string $replace
129     */
130    protected function matchRangeTokenStep2($i, $match, array &$matches, &$special, &$replace)
131    {
132        if (\in_array($match, array('', '<', '>', '=', ','), true)) {
133            $replace = \in_array($match, array('<', '>'), true) ? $match : $replace;
134            $matches[$i] = '~' === $special && \in_array($replace, array('<', '>'), true) ? '' : $matches[$i];
135        } elseif ('~' === $match) {
136            $special = $match;
137        } elseif (\in_array($match, array('EQUAL', '^'), true)) {
138            $special = $match;
139            $matches[$i] = '';
140        } else {
141            $this->matchRangeTokenStep3($i, $match, $matches, $special, $replace);
142        }
143    }
144
145    /**
146     * Step3: Converts the token of the matched range.
147     *
148     * @param int         $i
149     * @param string      $match
150     * @param array       $matches
151     * @param null|string $special
152     * @param null|string $replace
153     */
154    protected function matchRangeTokenStep3($i, $match, array &$matches, &$special, &$replace)
155    {
156        if (' ' === $match) {
157            $matches[$i] = ',';
158        } elseif ('||' === $match) {
159            $matches[$i] = '|';
160        } elseif (\in_array($special, array('^'), true)) {
161            $matches[$i] = SemverRangeUtil::replaceSpecialRange($this, $match);
162            $special = null;
163        } else {
164            $this->matchRangeTokenStep4($i, $match, $matches, $special, $replace);
165        }
166    }
167
168    /**
169     * Step4: Converts the token of the matched range.
170     *
171     * @param int         $i
172     * @param string      $match
173     * @param array       $matches
174     * @param null|string $special
175     * @param null|string $replace
176     */
177    protected function matchRangeTokenStep4($i, $match, array &$matches, &$special, &$replace)
178    {
179        if (',<~' === $special) {
180            // Version range contains x in last place.
181            $match .= (false === strpos($match, '.') ? '.x' : '');
182            $version = explode('.', $match);
183            $change = \count($version) - 2;
184            $version[$change] = (int) ($version[$change]) + 1;
185            $match = str_replace(array('*', 'x', 'X'), '0', implode('.', $version));
186        } elseif (null === $special && 0 === $i && false === strpos($match, '.') && is_numeric($match)) {
187            $match = isset($matches[$i + 1]) && (' - ' === $matches[$i + 1] || '-' === $matches[$i + 1])
188                ? $match
189                : '~'.$match;
190        } else {
191            $match = '~' === $special ? str_replace(array('*', 'x', 'X'), '0', $match) : $match;
192        }
193
194        $this->matchRangeTokenStep5($i, $match, $matches, $special, $replace);
195    }
196
197    /**
198     * Step5: Converts the token of the matched range.
199     *
200     * @param int         $i
201     * @param string      $match
202     * @param array       $matches
203     * @param null|string $special
204     * @param null|string $replace
205     */
206    protected function matchRangeTokenStep5($i, $match, array &$matches, &$special, &$replace)
207    {
208        $matches[$i] = $this->convertVersion($match);
209        $matches[$i] = $replace
210            ? SemverUtil::replaceAlias($matches[$i], $replace)
211            : $matches[$i];
212        $matches[$i] .= '~' === $special && \in_array($replace, array('<', '>'), true)
213            ? ','.$replace.$matches[$i]
214            : '';
215        $special = null;
216        $replace = null;
217    }
218}
219