1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Yosymfony\Spress. |
5
|
|
|
* |
6
|
|
|
* (c) YoSymfony <http://github.com/yosymfony> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Yosymfony\Spress\Core\ContentManager\Permalink; |
13
|
|
|
|
14
|
|
|
use Yosymfony\Spress\Core\DataSource\ItemInterface; |
15
|
|
|
use Yosymfony\Spress\Core\ContentManager\Exception\AttributeValueException; |
16
|
|
|
use Yosymfony\Spress\Core\ContentManager\Exception\MissingAttributeException; |
17
|
|
|
use Yosymfony\Spress\Core\Support\StringWrapper; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Iterface for a permalink generator. e.g: /my-page/about-me.html. |
21
|
|
|
* |
22
|
|
|
* Attributes with special meaning: |
23
|
|
|
* - permalink: (string) The permalink template. |
24
|
|
|
* - preserve_path_title: (bool) |
25
|
|
|
* - date: (string) |
26
|
|
|
* - categories: (array) |
27
|
|
|
* |
28
|
|
|
* Placeholders: |
29
|
|
|
* - ":path" : /my-page |
30
|
|
|
* - ":basename" : about-me |
31
|
|
|
* - ":extension" : html |
32
|
|
|
* |
33
|
|
|
* |
34
|
|
|
* @author Victor Puertas <[email protected]> |
35
|
|
|
*/ |
36
|
|
|
class PermalinkGenerator implements PermalinkGeneratorInterface |
37
|
|
|
{ |
38
|
|
|
/** |
39
|
|
|
* Predefined permalink 'none'. |
40
|
|
|
*/ |
41
|
|
|
const PERMALINK_NONE = '/:path/:basename.:extension'; |
42
|
|
|
/** |
43
|
|
|
* Predefined permalink template for 'date' & 'pretty' |
44
|
|
|
* '/:collection' gets prepended when in a custom collection. |
45
|
|
|
* 'pretty' also forces option 'no_html_extension'. |
46
|
|
|
*/ |
47
|
|
|
const PERMALINK_DATE = '/:categories/:year/:month/:day/:title.:extension'; |
48
|
|
|
/** |
49
|
|
|
* Predefined permalink 'ordinal'. |
50
|
|
|
*/ |
51
|
|
|
const PERMALINK_ORDINAL = '/:categories/:year/:i_day/:title.:extension'; |
52
|
|
|
|
53
|
|
|
private $defaultPermalink; |
54
|
|
|
private $defaultPreservePathTitle; |
55
|
|
|
private $defaultNoHtmlExtension; |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* Constructor. |
59
|
|
|
* |
60
|
|
|
* @param string $defaultPermalink |
61
|
|
|
* |
62
|
|
|
* Each item's URL are prefixed by "/:collection" if the item are included in a custom collection. |
63
|
|
|
* |
64
|
|
|
* "pretty" permalink style: |
65
|
|
|
* - item: "/:path/:basename" |
66
|
|
|
* - item with date: "/:categories/:year/:month/:day/:title/" |
67
|
|
|
* |
68
|
|
|
* "date" permalink style: |
69
|
|
|
* - item: "/:path/:basename.:extension" |
70
|
|
|
* - item with date: "/:categories/:year/:month/:day/:title.:extension" |
71
|
|
|
* |
72
|
|
|
* "ordinal" permalink style: |
73
|
|
|
* - item: "/:path/:basename.:extension" |
74
|
|
|
* - item with date: "/:categories/:year/:i_day/:title.:extension" |
75
|
|
|
* |
76
|
|
|
* "none" permalink style: |
77
|
|
|
* - item: "/:path/:basename.:extension" |
78
|
|
|
* @param bool $defaultPreservePathTitle Default value for Preserve-path-title |
79
|
|
|
* @param bool $defaultNoHtmlExtension Default value for no-html-extension |
80
|
|
|
*/ |
81
|
|
|
public function __construct( |
82
|
|
|
$defaultPermalink = 'pretty', |
83
|
|
|
$defaultPreservePathTitle = false, |
84
|
|
|
$defaultNoHtmlExtension = false |
85
|
|
|
) { |
86
|
|
|
$this->defaultPermalink = $defaultPermalink; |
87
|
|
|
$this->defaultPreservePathTitle = $defaultPreservePathTitle; |
88
|
|
|
$this->defaultNoHtmlExtension = $defaultNoHtmlExtension; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Gets a permalink. This method uses the SNAPSHOT_PATH_RELATIVE_AFTER_CONVERT of Item path. |
93
|
|
|
* |
94
|
|
|
* For binary items URL path and path point to SNAPSHOT_PATH_RELATIVE. |
95
|
|
|
* |
96
|
|
|
* Item's attributes with special meaning: |
97
|
|
|
* - title: title of the item. |
98
|
|
|
* - title_path: title extracted from the date filename pattern. |
99
|
|
|
* - preserve_path_title: if true "title_path" instead of "title" will be used with ":title" placeholder. |
100
|
|
|
* - date: date of item. |
101
|
|
|
* - categories: categories for the item |
102
|
|
|
* - permalink: permalink sytle. |
103
|
|
|
* - collection: the name of the item's collection. |
104
|
|
|
* |
105
|
|
|
* @param \Yosymfony\Spress\Core\DataSource\ItemInterface $item |
106
|
|
|
* |
107
|
|
|
* @return \Yosymfony\Spress\Core\ContentManager\Permalink\PermalinkInterface |
108
|
|
|
*/ |
109
|
|
|
public function getPermalink(ItemInterface $item) |
110
|
|
|
{ |
111
|
|
|
if ($item->getPath(ItemInterface::SNAPSHOT_PATH_RELATIVE_AFTER_CONVERT) === '') { |
112
|
|
|
return new Permalink('', ''); |
113
|
|
|
} |
114
|
|
|
|
115
|
|
|
$placeholders = $this->getPlacehoders($item); |
116
|
|
|
$permalinkStyle = $this->getPermalinkAttribute($item); |
117
|
|
|
$noHtmlExtension = $this->getNoHtmlExtensionAttribute($item); |
118
|
|
|
|
119
|
|
|
if ($item->isBinary() === true) { |
120
|
|
|
$urlTemplate = $this::PERMALINK_NONE; |
121
|
|
|
$path = $this->generatePath($urlTemplate, $placeholders); |
122
|
|
|
$urlPath = $this->generateUrlPath($urlTemplate, $placeholders); |
123
|
|
|
|
124
|
|
|
return new Permalink($path, $urlPath); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
switch ($permalinkStyle) { |
128
|
|
|
case 'none': |
129
|
|
|
$urlTemplate = $this::PERMALINK_NONE; |
130
|
|
|
break; |
131
|
|
View Code Duplication |
case 'ordinal': |
|
|
|
|
132
|
|
|
if ($this->isItemWithDate($item) === true) { |
133
|
|
|
$urlTemplate = $this::PERMALINK_ORDINAL; |
134
|
|
|
|
135
|
|
|
if ($this->isCustomCollection($item)) { |
136
|
|
|
$urlTemplate = '/:collection'.$urlTemplate; |
137
|
|
|
} |
138
|
|
|
} else { |
139
|
|
|
$urlTemplate = $this::PERMALINK_NONE; |
140
|
|
|
} |
141
|
|
|
break; |
142
|
|
|
case 'pretty': |
143
|
|
|
$noHtmlExtension = true; |
144
|
|
|
// no break |
145
|
|
View Code Duplication |
case 'date': |
|
|
|
|
146
|
|
|
if ($this->isItemWithDate($item) === true) { |
147
|
|
|
$urlTemplate = $this::PERMALINK_DATE; |
148
|
|
|
|
149
|
|
|
if ($this->isCustomCollection($item)) { |
150
|
|
|
$urlTemplate = '/:collection'.$urlTemplate; |
151
|
|
|
} |
152
|
|
|
} else { |
153
|
|
|
$urlTemplate = $this::PERMALINK_NONE; |
154
|
|
|
} |
155
|
|
|
break; |
156
|
|
|
default: |
157
|
|
|
if ($this->templateNeedsDate($permalinkStyle) === false || $this->isItemWithDate($item) === true) { |
158
|
|
|
$urlTemplate = $permalinkStyle; |
159
|
|
|
} else { |
160
|
|
|
$urlTemplate = $this::PERMALINK_NONE; |
161
|
|
|
} |
162
|
|
|
break; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
if ($noHtmlExtension && $placeholders[':extension'] === 'html') { |
166
|
|
|
if ($placeholders[':basename'] === 'index') { |
167
|
|
|
$placeholders[':basename'] = ''; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
$urlTemplate = str_replace(['.:extension', ':extension'], '', $urlTemplate); |
171
|
|
|
$pathTemplate = $urlTemplate.'/index.html'; |
172
|
|
|
} else { |
173
|
|
|
$pathTemplate = $urlTemplate; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
$path = $this->generatePath($pathTemplate, $placeholders); |
177
|
|
|
$urlPath = $this->generateUrlPath($urlTemplate, $placeholders); |
178
|
|
|
|
179
|
|
|
return new Permalink($path, $urlPath); |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
private function getPlacehoders(ItemInterface $item) |
183
|
|
|
{ |
184
|
|
|
$fileInfo = new \SplFileInfo($item->getPath(ItemInterface::SNAPSHOT_PATH_RELATIVE_AFTER_CONVERT)); |
185
|
|
|
|
186
|
|
|
$result = [ |
187
|
|
|
':path' => (new StringWrapper($fileInfo->getPath()))->deletePrefix('.'), |
188
|
|
|
':extension' => $fileInfo->getExtension(), |
189
|
|
|
':basename' => $fileInfo->getBasename('.'.$fileInfo->getExtension()), |
190
|
|
|
':collection' => $item->getCollection(), |
191
|
|
|
':categories' => $this->getCategoriesPath($item), |
192
|
|
|
':title' => $this->getTitleSlugified($item), |
193
|
|
|
]; |
194
|
|
|
|
195
|
|
|
if ($this->isItemWithDate($item)) { |
196
|
|
|
$time = $this->getDateAttribute($item); |
197
|
|
|
$result += [ |
198
|
|
|
':year' => $time->format('Y'), |
199
|
|
|
':month' => $time->format('m'), |
200
|
|
|
':day' => $time->format('d'), |
201
|
|
|
':i_month' => $time->format('n'), |
202
|
|
|
':i_day' => $time->format('j'), |
203
|
|
|
]; |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
return $result; |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
private function isItemWithDate(ItemInterface $item) |
210
|
|
|
{ |
211
|
|
|
$attributes = $item->getAttributes(); |
212
|
|
|
|
213
|
|
|
return isset($attributes['date']); |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
private function templateNeedsDate($template) |
217
|
|
|
{ |
218
|
|
|
return strpos($template, ':year') !== false |
219
|
|
|
|| strpos($template, ':month') !== false |
220
|
|
|
|| strpos($template, ':day') !== false |
221
|
|
|
|| strpos($template, ':i_month') !== false |
222
|
|
|
|| strpos($template, ':i_day') !== false; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
private function getTitleSlugified(ItemInterface $item) |
226
|
|
|
{ |
227
|
|
|
$attributes = $item->getAttributes(); |
228
|
|
|
|
229
|
|
|
$preservePathTitle = $this->getPreservePathTitleAttribute($item); |
230
|
|
|
|
231
|
|
|
if ($preservePathTitle === true && isset($attributes['title_path']) === true) { |
232
|
|
|
return rawurlencode($attributes['title_path']); |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
if (isset($attributes['title']) === true) { |
236
|
|
|
return (new StringWrapper($attributes['title']))->slug(); |
237
|
|
|
} |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
private function getCategoriesPath(ItemInterface $item) |
241
|
|
|
{ |
242
|
|
|
$attributes = $item->getAttributes(); |
243
|
|
|
|
244
|
|
|
if (isset($attributes['categories']) === false) { |
245
|
|
|
return; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
if (is_array($attributes['categories']) === false) { |
249
|
|
|
throw new AttributeValueException('Invalid value. Expected array.', 'categories', $item->getPath()); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
return implode('/', array_map(function ($a) { |
253
|
|
|
return (new StringWrapper($a))->slug(); |
254
|
|
|
}, $attributes['categories'])); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
private function generatePath($template, array $placeholders = []) |
258
|
|
|
{ |
259
|
|
|
$path = $this->generateUrlPath($template, $placeholders); |
260
|
|
|
|
261
|
|
|
return ltrim($path, '/'); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
private function generateUrlPath($template, array $placeholders = []) |
265
|
|
|
{ |
266
|
|
|
if (0 == strlen($template)) { |
267
|
|
|
throw new \InvalidArgumentException('The template param must be a template or a URL.'); |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
$permalink = str_replace(array_keys($placeholders), $placeholders, $template, $count); |
271
|
|
|
|
272
|
|
|
return $this->sanitize($permalink); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
View Code Duplication |
private function getPreservePathTitleAttribute(ItemInterface $item) |
|
|
|
|
276
|
|
|
{ |
277
|
|
|
$attributes = $item->getAttributes(); |
278
|
|
|
|
279
|
|
|
if (isset($attributes['preserve_path_title']) === true) { |
280
|
|
|
if (is_bool($attributes['preserve_path_title']) === false) { |
281
|
|
|
throw new AttributeValueException('Invalid value. Expected bolean.', 'preserve_path_title', $item->getPath()); |
282
|
|
|
} |
283
|
|
|
|
284
|
|
|
return $attributes['preserve_path_title']; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
return $this->defaultPreservePathTitle; |
288
|
|
|
} |
289
|
|
|
|
290
|
|
|
private function getDateAttribute(ItemInterface $item) |
291
|
|
|
{ |
292
|
|
|
$attributes = $item->getAttributes(); |
293
|
|
|
|
294
|
|
|
if (isset($attributes['date']) === false) { |
295
|
|
|
throw new MissingAttributeException('Attribute date required.', 'date', $item->getPath()); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
if (is_string($attributes['date']) === false) { |
299
|
|
|
throw new AttributeValueException('Invalid value. Expected date string.', 'date', $item->getPath()); |
300
|
|
|
} |
301
|
|
|
|
302
|
|
|
try { |
303
|
|
|
return new \DateTime($attributes['date']); |
304
|
|
|
} catch (\Exception $e) { |
305
|
|
|
throw new AttributeValueException('Invalid value. Expected date string.', 'date', $item->getPath()); |
306
|
|
|
} |
307
|
|
|
} |
308
|
|
|
|
309
|
|
View Code Duplication |
private function getNoHtmlExtensionAttribute(ItemInterface $item) |
|
|
|
|
310
|
|
|
{ |
311
|
|
|
$attributes = $item->getAttributes(); |
312
|
|
|
|
313
|
|
|
if (isset($attributes['no_html_extension']) === true) { |
314
|
|
|
if (is_bool($attributes['no_html_extension']) === false) { |
315
|
|
|
throw new AttributeValueException('Invalid value. Expected boolean.', 'no_html_extension', $item->getPath()); |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
return $attributes['no_html_extension']; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
return $this->defaultNoHtmlExtension; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
private function isCustomCollection(ItemInterface $item) |
325
|
|
|
{ |
326
|
|
|
return !in_array($item->getCollection(), ['posts', 'pages']); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
private function getPermalinkAttribute(ItemInterface $item) |
330
|
|
|
{ |
331
|
|
|
$attributes = $item->getAttributes(); |
332
|
|
|
$permalink = isset($attributes['permalink']) ? $attributes['permalink'] : $this->defaultPermalink; |
333
|
|
|
|
334
|
|
|
if (is_string($permalink) === false) { |
335
|
|
|
throw new AttributeValueException('Invalid value. Expected string.', 'permalink', $item->getPath()); |
336
|
|
|
} |
337
|
|
|
|
338
|
|
|
if (trim($permalink) === '') { |
339
|
|
|
throw new AttributeValueException('Invalid value. Expected a non-empty value.', 'permalink', $item->getPath()); |
340
|
|
|
} |
341
|
|
|
|
342
|
|
|
return $permalink; |
343
|
|
|
} |
344
|
|
|
|
345
|
|
|
private function sanitize($url) |
346
|
|
|
{ |
347
|
|
|
$count = 0; |
348
|
|
|
|
349
|
|
|
if ((new StringWrapper($url))->startWith('/') === false) { |
350
|
|
|
$url = '/'.$url; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
$result = preg_replace('/\/\/+/', '/', $url); |
354
|
|
|
$result = str_replace(':/', '://', $result, $count); |
355
|
|
|
|
356
|
|
|
if ($result !== '/') { |
357
|
|
|
$result = rtrim($result, '/'); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
if ($count > 1) { |
361
|
|
|
throw new \UnexpectedValueException(sprintf('Bad URL: "%s".', $result)); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
if (false !== strpos($result, ' ')) { |
365
|
|
|
throw new \UnexpectedValueException(sprintf('Bad URL: "%s". Contain white space/s.', $result)); |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
return $result; |
369
|
|
|
} |
370
|
|
|
} |
371
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.