-
Notifications
You must be signed in to change notification settings - Fork 50
/
IterableCodeExtractor.php
255 lines (211 loc) · 8.09 KB
/
IterableCodeExtractor.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
<?php
namespace WP_CLI\I18n;
use Gettext\Translation;
use Gettext\Translations;
use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use WP_CLI;
use WP_CLI\Utils;
trait IterableCodeExtractor {
private static $dir = '';
/**
* Extract the translations from a file.
*
* @param array|string $file A path of a file or files
* @param Translations $translations The translations instance to append the new translations.
* @param array $options {
* Optional. An array of options passed down to static::fromString()
*
* @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'.
* }
* @return null
*/
public static function fromFile( $file, Translations $translations, array $options = [] ) {
foreach ( static::getFiles( $file ) as $f ) {
// Make sure a relative file path is added as a comment.
$options['file'] = ltrim( str_replace( static::$dir, '', Utils\normalize_path( $f ) ), '/' );
$string = file_get_contents( $f );
if ( ! $string ) {
WP_CLI::debug(
sprintf(
'Could not load file %1s',
$f
)
);
continue;
}
if ( ! empty( $options['wpExtractTemplates'] ) ) {
$headers = MakePotCommand::get_file_data_from_string( $string, [ 'Template Name' => 'Template Name' ] );
if ( ! empty( $headers['Template Name'] ) ) {
$translation = new Translation( '', $headers['Template Name'] );
$translation->addExtractedComment( 'Template Name of the theme' );
$translations[] = $translation;
}
}
static::fromString( $string, $translations, $options );
}
}
/**
* Extract the translations from a file.
*
* @param string $dir Root path to start the recursive traversal in.
* @param Translations $translations The translations instance to append the new translations.
* @param array $options {
* Optional. An array of options passed down to static::fromString()
*
* @type bool $wpExtractTemplates Extract 'Template Name' headers in theme files. Default 'false'.
* @type array $exclude A list of path to exclude. Default [].
* @type array $extensions A list of extensions to process. Default [].
* }
* @return null
*/
public static function fromDirectory( $dir, Translations $translations, array $options = [] ) {
$dir = Utils\normalize_path( $dir );
static::$dir = $dir;
$include = isset( $options['include'] ) ? $options['include'] : [];
$exclude = isset( $options['exclude'] ) ? $options['exclude'] : [];
$files = static::getFilesFromDirectory( $dir, $include, $exclude, $options['extensions'] );
if ( ! empty( $files ) ) {
static::fromFile( $files, $translations, $options );
}
static::$dir = '';
}
/**
* Determines whether a file is valid based on given matchers.
*
* @param SplFileInfo $file File or directory.
* @param array $matchers List of files and directories to match.
* @return int How strongly the file is matched.
*/
protected static function calculateMatchScore( SplFileInfo $file, array $matchers = [] ) {
if ( empty( $matchers ) ) {
return 0;
}
if ( in_array( $file->getBasename(), $matchers, true ) ) {
return 10;
}
// Check for more complex paths, e.g. /some/sub/folder.
$root_relative_path = str_replace( static::$dir, '', $file->getPathname() );
foreach ( $matchers as $path_or_file ) {
$pattern = preg_quote( str_replace( '*', '__wildcard__', $path_or_file ), '/' );
$pattern = '(^|/)' . str_replace( '__wildcard__', '(.+)', $pattern );
// Base score is the amount of nested directories, discounting wildcards.
$base_score = count(
array_filter(
explode( '/', $path_or_file ),
function ( $component ) {
return '*' !== $component;
}
)
);
if ( 0 === $base_score ) {
// If the matcher is simply * it gets a score above the implicit score but below 1.
$base_score = 0.2;
}
// If the matcher contains no wildcards and matches the end of the path.
if (
false === strpos( $path_or_file, '*' ) &&
false !== mb_ereg( $pattern . '$', $root_relative_path )
) {
return $base_score * 10;
}
// If the matcher matches the end of the path or a full directory contained.
if ( false !== mb_ereg( $pattern . '(/|$)', $root_relative_path ) ) {
return $base_score;
}
}
return 0;
}
/**
* Determines whether or not a directory has children that may be matched.
*
* @param SplFileInfo $dir Directory.
* @param array $matchers List of files and directories to match.
* @return bool Whether or not there are any matchers for children of this directory.
*/
protected static function containsMatchingChildren( SplFileInfo $dir, array $matchers = [] ) {
if ( empty( $matchers ) ) {
return false;
}
/** @var string $root_relative_path */
$root_relative_path = str_replace( static::$dir, '', $dir->getPathname() );
foreach ( $matchers as $path_or_file ) {
// If the matcher contains no wildcards and the path matches the start of the matcher.
if (
'' !== $root_relative_path &&
false === strpos( $path_or_file, '*' ) &&
0 === strpos( $path_or_file . '/', $root_relative_path )
) {
return true;
}
$base = current( explode( '*', $path_or_file ) );
// If start of the path matches the start of the matcher until the first wildcard.
// Or the start of the matcher until the first wildcard matches the start of the path.
if (
( '' !== $root_relative_path && 0 === strpos( $base, $root_relative_path ) ) ||
( '' !== $base && 0 === strpos( $root_relative_path, $base ) )
) {
return true;
}
}
return false;
}
/**
* Recursively gets all PHP files within a directory.
*
* @param string $dir A path of a directory.
* @param array $include List of files and directories to include.
* @param array $exclude List of files and directories to skip.
* @param array $extensions List of filename extensions to process.
*
* @return array File list.
*/
public static function getFilesFromDirectory( $dir, array $include = [], array $exclude = [], $extensions = [] ) {
$filtered_files = [];
$files = new RecursiveIteratorIterator(
new RecursiveCallbackFilterIterator(
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS | RecursiveDirectoryIterator::UNIX_PATHS ),
function ( $file, $key, $iterator ) use ( $include, $exclude, $extensions ) {
/** @var RecursiveCallbackFilterIterator $iterator */
/** @var SplFileInfo $file */
// Normalize include and exclude paths.
$include = array_map( 'static::trim_leading_slash', $include );
$exclude = array_map( 'static::trim_leading_slash', $exclude );
// If no $include is passed everything gets the weakest possible matching score.
$inclusion_score = empty( $include ) ? 0.1 : static::calculateMatchScore( $file, $include );
$exclusion_score = static::calculateMatchScore( $file, $exclude );
// Always include directories that aren't excluded.
if ( 0 === $exclusion_score && $iterator->hasChildren() ) {
return true;
}
if ( ( 0 === $inclusion_score || $exclusion_score > $inclusion_score ) && $iterator->hasChildren() ) {
// Always include directories that may have matching children even if they are excluded.
return static::containsMatchingChildren( $file, $include );
}
return ( ( $inclusion_score >= $exclusion_score ) && $file->isFile() && in_array( $file->getExtension(), $extensions, true ) );
}
),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ( $files as $file ) {
/** @var SplFileInfo $file */
if ( ! $file->isFile() || ! in_array( $file->getExtension(), $extensions, true ) ) {
continue;
}
$filtered_files[] = Utils\normalize_path( $file->getPathname() );
}
sort( $filtered_files, SORT_NATURAL | SORT_FLAG_CASE );
return $filtered_files;
}
/**
* Trim leading slash from a path.
*
* @param string $path Path to trim.
* @return string Trimmed path.
*/
private static function trim_leading_slash( $path ) {
return ltrim( $path, '/' );
}
}