1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
/** |
5
|
|
|
* This file is part of phpDocumentor. |
6
|
|
|
* |
7
|
|
|
* For the full copyright and license information, please view the LICENSE |
8
|
|
|
* file that was distributed with this source code. |
9
|
|
|
* |
10
|
|
|
* @author Mike van Riel <[email protected]> |
11
|
|
|
* @copyright 2010-2018 Mike van Riel / Naenius (http://www.naenius.com) |
12
|
|
|
* @license http://www.opensource.org/licenses/mit-license.php MIT |
13
|
|
|
* @link http://phpdoc.org |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
namespace phpDocumentor\Transformer\Writer; |
17
|
|
|
|
18
|
|
|
use InvalidArgumentException; |
19
|
|
|
use phpDocumentor\Descriptor\DescriptorAbstract; |
20
|
|
|
use phpDocumentor\Descriptor\ProjectDescriptor; |
21
|
|
|
use phpDocumentor\Transformer\Writer\Twig\Extension; |
22
|
|
|
use phpDocumentor\Transformer\Router\ForFileProxy; |
23
|
|
|
use phpDocumentor\Transformer\Router\Queue; |
24
|
|
|
use phpDocumentor\Transformer\Transformation; |
25
|
|
|
use Twig_Environment; |
26
|
|
|
use Twig_Extension_Debug; |
27
|
|
|
use Twig_Loader_Filesystem; |
28
|
|
|
use UnexpectedValueException; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* A specialized writer which uses the Twig templating engine to convert |
32
|
|
|
* templates to HTML output. |
33
|
|
|
* |
34
|
|
|
* This writer support the Query attribute of a Transformation to generate |
35
|
|
|
* multiple templates in one transformation. |
36
|
|
|
* |
37
|
|
|
* The Query attribute supports a simplified version of Twig queries and will |
38
|
|
|
* use each individual result as the 'node' global variable in the Twig template. |
39
|
|
|
* |
40
|
|
|
* Example: |
41
|
|
|
* |
42
|
|
|
* Suppose a Query `indexes.classes` is given then this writer will be |
43
|
|
|
* invoked as many times as there are classes in the project and the |
44
|
|
|
* 'node' global variable in twig will be filled with each individual |
45
|
|
|
* class entry. |
46
|
|
|
* |
47
|
|
|
* When using the Query attribute in the transformation it is important to |
48
|
|
|
* use a variable in the Artifact attribute as well (otherwise the same file |
49
|
|
|
* would be overwritten several times). |
50
|
|
|
* |
51
|
|
|
* A simple example transformation line could be: |
52
|
|
|
* |
53
|
|
|
* ``` |
54
|
|
|
* <transformation |
55
|
|
|
* writer="twig" |
56
|
|
|
* source="templates/twig/index.twig" |
57
|
|
|
* artifact="index.html"/> |
58
|
|
|
* ``` |
59
|
|
|
* |
60
|
|
|
* This example transformation would use this writer to transform the |
61
|
|
|
* index.twig template file in the twig template folder into index.html at |
62
|
|
|
* the destination location. |
63
|
|
|
* Since no Query is provided the 'node' global variable will contain |
64
|
|
|
* the Project Descriptor of the Object Graph. |
65
|
|
|
* |
66
|
|
|
* A complex example transformation line could be: |
67
|
|
|
* |
68
|
|
|
* ``` |
69
|
|
|
* <transformation |
70
|
|
|
* query="indexes.classes" |
71
|
|
|
* writer="twig" |
72
|
|
|
* source="templates/twig/class.twig" |
73
|
|
|
* artifact="{{name}}.html"/> |
74
|
|
|
* ``` |
75
|
|
|
* |
76
|
|
|
* This example transformation would use this writer to transform the |
77
|
|
|
* class.twig template file in the twig template folder into a file with |
78
|
|
|
* the 'name' property for an individual class inside the Object Graph. |
79
|
|
|
* Since a Query *is* provided will the 'node' global variable contain a |
80
|
|
|
* specific instance of a class applicable to the current iteration. |
81
|
|
|
* |
82
|
|
|
* @see self::getDestinationPath() for more information about variables in the |
83
|
|
|
* Artifact attribute. |
84
|
|
|
*/ |
85
|
|
|
class Twig extends WriterAbstract implements Routable |
86
|
|
|
{ |
87
|
|
|
/** @var Queue $routers */ |
88
|
|
|
protected $routers; |
89
|
|
|
|
90
|
|
|
/** @var Twig_Environment $twig */ |
91
|
|
|
private $twig; |
92
|
|
|
|
93
|
|
|
public function __construct(Twig_Environment $twig) |
94
|
|
|
{ |
95
|
|
|
$this->twig = $twig; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* This method combines the ProjectDescriptor and the given target template |
100
|
|
|
* and creates a static html page at the artifact location. |
101
|
|
|
* |
102
|
|
|
* @param ProjectDescriptor $project Document containing the structure. |
103
|
|
|
* @param Transformation $transformation Transformation to execute. |
104
|
|
|
*/ |
105
|
|
|
public function transform(ProjectDescriptor $project, Transformation $transformation): void |
106
|
|
|
{ |
107
|
|
|
$template_path = $this->getTemplatePath($transformation); |
108
|
|
|
|
109
|
|
|
$finder = new Pathfinder(); |
110
|
|
|
$nodes = $finder->find($project, $transformation->getQuery()); |
111
|
|
|
|
112
|
|
|
foreach ($nodes as $node) { |
113
|
|
|
if (!$node) { |
114
|
|
|
continue; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
$destination = $this->getDestinationPath($node, $transformation); |
118
|
|
|
if ($destination === false) { |
119
|
|
|
continue; |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
$environment = $this->initializeEnvironment($project, $transformation, $destination); |
123
|
|
|
$environment->addGlobal('node', $node); |
124
|
|
|
|
125
|
|
|
$html = $environment->render(substr($transformation->getSource(), strlen($template_path))); |
126
|
|
|
file_put_contents($destination, $html); |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* Initializes the Twig environment with the template, base extension and additionally defined extensions. |
132
|
|
|
*/ |
133
|
|
|
protected function initializeEnvironment( |
134
|
|
|
ProjectDescriptor $project, |
135
|
|
|
Transformation $transformation, |
136
|
|
|
string $destination |
137
|
|
|
): Twig_Environment { |
138
|
|
|
$callingTemplatePath = $this->getTemplatePath($transformation); |
139
|
|
|
|
140
|
|
|
$baseTemplatesPath = $transformation->getTransformer()->getTemplates()->getTemplatesPath(); |
141
|
|
|
|
142
|
|
|
$templateFolders = [ |
143
|
|
|
$baseTemplatesPath . '/..' . DIRECTORY_SEPARATOR . $callingTemplatePath, |
144
|
|
|
// http://twig.sensiolabs.org/doc/recipes.html#overriding-a-template-that-also-extends-itself |
145
|
|
|
$baseTemplatesPath, |
146
|
|
|
]; |
147
|
|
|
|
148
|
|
|
// get all invoked template paths, they overrule the calling template path |
149
|
|
|
/** @var \phpDocumentor\Transformer\Template $template */ |
150
|
|
|
foreach ($transformation->getTransformer()->getTemplates() as $template) { |
151
|
|
|
$path = $baseTemplatesPath . DIRECTORY_SEPARATOR . $template->getName(); |
152
|
|
|
array_unshift($templateFolders, $path); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
|
// Clone twig because otherwise we cannot re-set the extensions on this twig environment on every run of this |
156
|
|
|
// writer |
157
|
|
|
$env = clone $this->twig; |
158
|
|
|
$env->setLoader(new Twig_Loader_Filesystem($templateFolders)); |
159
|
|
|
|
160
|
|
|
$this->addPhpDocumentorExtension($project, $transformation, $destination, $env); |
161
|
|
|
$this->addExtensionsFromTemplateConfiguration($transformation, $project, $env); |
162
|
|
|
|
163
|
|
|
return $env; |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Adds the phpDocumentor base extension to the Twig Environment. |
168
|
|
|
*/ |
169
|
|
|
protected function addPhpDocumentorExtension( |
170
|
|
|
ProjectDescriptor $project, |
171
|
|
|
Transformation $transformation, |
172
|
|
|
string $destination, |
173
|
|
|
Twig_Environment $twigEnvironment |
174
|
|
|
): void { |
175
|
|
|
$base_extension = new Extension($project, $transformation); |
176
|
|
|
$base_extension->setDestination( |
177
|
|
|
substr($destination, strlen($transformation->getTransformer()->getTarget()) + 1) |
178
|
|
|
); |
179
|
|
|
$base_extension->setRouters($this->routers); |
180
|
|
|
$twigEnvironment->addExtension($base_extension); |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Tries to add any custom extensions that have been defined in the template or the transformation's configuration. |
185
|
|
|
* |
186
|
|
|
* This method will read the `twig-extension` parameter of the transformation (which inherits the template's |
187
|
|
|
* parameter set) and try to add those extensions to the environment. |
188
|
|
|
* |
189
|
|
|
* @throws InvalidArgumentException if a twig-extension should be loaded but it could not be found. |
190
|
|
|
*/ |
191
|
|
|
protected function addExtensionsFromTemplateConfiguration( |
192
|
|
|
Transformation $transformation, |
193
|
|
|
ProjectDescriptor $project, |
194
|
|
|
Twig_Environment $twigEnvironment |
195
|
|
|
): void { |
196
|
|
|
$isDebug = $transformation->getParameter('twig-debug') |
197
|
|
|
? $transformation->getParameter('twig-debug')->getValue() |
198
|
|
|
: false; |
199
|
|
|
if ($isDebug === 'true') { |
200
|
|
|
$twigEnvironment->enableDebug(); |
201
|
|
|
$twigEnvironment->enableAutoReload(); |
202
|
|
|
$twigEnvironment->addExtension(new Twig_Extension_Debug()); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** @var \phpDocumentor\Transformer\Template\Parameter $extension */ |
206
|
|
|
foreach ($transformation->getParametersWithKey('twig-extension') as $extension) { |
207
|
|
|
$extensionValue = $extension->getValue(); |
208
|
|
|
if (!class_exists($extensionValue)) { |
209
|
|
|
throw new InvalidArgumentException('Unknown twig extension: ' . $extensionValue); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
// to support 'normal' Twig extensions we check the interface to determine what instantiation to do. |
213
|
|
|
$implementsInterface = in_array( |
214
|
|
|
'phpDocumentor\Transformer\Writer\Twig\ExtensionInterface', |
215
|
|
|
class_implements($extensionValue), |
216
|
|
|
true |
217
|
|
|
); |
218
|
|
|
|
219
|
|
|
$twigEnvironment->addExtension( |
220
|
|
|
$implementsInterface ? new $extensionValue($project, $transformation) : new $extensionValue() |
221
|
|
|
); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
/** |
226
|
|
|
* Uses the currently selected node and transformation to assemble the destination path for the file. |
227
|
|
|
* |
228
|
|
|
* The Twig writer accepts the use of a Query to be able to generate output for multiple objects using the same |
229
|
|
|
* template. |
230
|
|
|
* |
231
|
|
|
* The given node is the result of such a query, or if no query given the selected element, and the transformation |
232
|
|
|
* contains the destination file. |
233
|
|
|
* |
234
|
|
|
* Since it is important to be able to generate a unique name per element can the user provide a template variable |
235
|
|
|
* in the name of the file. |
236
|
|
|
* Such a template variable always resides between double braces and tries to take the node value of a given |
237
|
|
|
* query string. |
238
|
|
|
* |
239
|
|
|
* Example: |
240
|
|
|
* |
241
|
|
|
* An artifact stating `classes/{{name}}.html` will try to find the |
242
|
|
|
* node 'name' as a child of the given $node and use that value instead. |
243
|
|
|
* |
244
|
|
|
* @param DescriptorAbstract|ProjectDescriptor $node |
245
|
|
|
* @throws InvalidArgumentException if no artifact is provided and no routing rule matches. |
246
|
|
|
* @throws UnexpectedValueException if the provided node does not contain anything. |
247
|
|
|
* @return false|string returns the destination location or false if generation should be aborted. |
248
|
|
|
*/ |
249
|
|
|
protected function getDestinationPath($node, Transformation $transformation) |
250
|
|
|
{ |
251
|
|
|
$writer = $this; |
|
|
|
|
252
|
|
|
|
253
|
|
|
if (!$node) { |
254
|
|
|
throw new UnexpectedValueException( |
255
|
|
|
'The transformation node in the twig writer is not expected to be false or null' |
256
|
|
|
); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
if (!$transformation->getArtifact()) { |
260
|
|
|
$rule = $this->routers->match($node); |
|
|
|
|
261
|
|
|
if (!$rule) { |
262
|
|
|
throw new InvalidArgumentException( |
263
|
|
|
'No matching routing rule could be found for the given node, please provide an artifact location, ' |
264
|
|
|
. 'encountered: ' . ($node === null ? 'NULL' : get_class($node)) |
265
|
|
|
); |
266
|
|
|
} |
267
|
|
|
|
268
|
|
|
$rule = new ForFileProxy($rule); |
269
|
|
|
$url = $rule->generate($node); |
|
|
|
|
270
|
|
|
if ($url === false || $url[0] !== DIRECTORY_SEPARATOR) { |
271
|
|
|
return false; |
272
|
|
|
} |
273
|
|
|
|
274
|
|
|
$path = $transformation->getTransformer()->getTarget() . str_replace('/', DIRECTORY_SEPARATOR, $url); |
275
|
|
|
} else { |
276
|
|
|
$path = $transformation->getTransformer()->getTarget() |
277
|
|
|
. DIRECTORY_SEPARATOR . $transformation->getArtifact(); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
$finder = new Pathfinder(); |
281
|
|
|
$destination = preg_replace_callback( |
282
|
|
|
'/{{([^}]+)}}/', // explicitly do not use the unicode modifier; this breaks windows |
283
|
|
|
function ($query) use ($node, $finder) { |
284
|
|
|
// strip any surrounding \ or / |
285
|
|
|
$filepart = trim((string) current($finder->find($node, $query[1])), '\\/'); |
286
|
|
|
|
287
|
|
|
// make it windows proof |
288
|
|
|
if (extension_loaded('iconv')) { |
289
|
|
|
$filepart = iconv('UTF-8', 'ASCII//TRANSLIT', $filepart); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
return strpos($filepart, '/') !== false |
293
|
|
|
? implode('/', array_map('urlencode', explode('/', $filepart))) |
294
|
|
|
: implode('\\', array_map('urlencode', explode('\\', $filepart))); |
295
|
|
|
}, |
296
|
|
|
$path |
297
|
|
|
); |
298
|
|
|
|
299
|
|
|
// replace any \ with the directory separator to be compatible with the |
300
|
|
|
// current filesystem and allow the next file_exists to do its work |
301
|
|
|
$destination = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $destination); |
302
|
|
|
|
303
|
|
|
// create directory if it does not exist yet |
304
|
|
|
if (!file_exists(dirname($destination))) { |
305
|
|
|
mkdir(dirname($destination), 0777, true); |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $destination; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* Returns the path belonging to the template. |
313
|
|
|
*/ |
314
|
|
|
protected function getTemplatePath(Transformation $transformation): string |
315
|
|
|
{ |
316
|
|
|
$parts = preg_split('[\\\\|/]', $transformation->getSource()); |
317
|
|
|
|
318
|
|
|
return $parts[0] . DIRECTORY_SEPARATOR . $parts[1]; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
public function setRouters(Queue $routers): void |
322
|
|
|
{ |
323
|
|
|
$this->routers = $routers; |
324
|
|
|
} |
325
|
|
|
} |
326
|
|
|
|
This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.
Both the
$myVar
assignment in line 1 and the$higher
assignment in line 2 are dead. The first because$myVar
is never used and the second because$higher
is always overwritten for every possible time line.