GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

DocGen   D
last analyzed

Complexity

Total Complexity 58

Size/Duplication

Total Lines 331
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 216
c 7
b 0
f 0
dl 0
loc 331
rs 4.5599
wmc 58

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
F gen() 0 271 49
A parse() 0 11 2
A generateContribIndex() 0 13 3
A generateRecipesIndex() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like DocGen often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocGen, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/* (c) Anton Medvedev <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Deployer\Documentation;
12
13
use RecursiveDirectoryIterator;
14
use RecursiveIteratorIterator;
15
use RecursiveRegexIterator;
16
use RegexIterator;
17
18
class DocGen
19
{
20
    /**
21
     * @var string
22
     */
23
    public $root;
24
    /**
25
     * @var DocRecipe[]
26
     */
27
    public $recipes = [];
28
29
    public function __construct(string $root)
30
    {
31
        $this->root = str_replace(DIRECTORY_SEPARATOR, '/', realpath($root));
32
    }
33
34
    public function parse(string $source): void
35
    {
36
        $directory = new RecursiveDirectoryIterator($source);
37
        $iterator = new RegexIterator(new RecursiveIteratorIterator($directory), '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH);
38
        foreach ($iterator as [$path]) {
39
            $realPath = str_replace(DIRECTORY_SEPARATOR, '/', realpath($path));
40
            $recipePath = str_replace($this->root . '/', '', $realPath);
41
            $recipeName = preg_replace('/\.php$/i', '', basename($recipePath));
42
            $recipe = new DocRecipe($recipeName, $recipePath);
43
            $recipe->parse(file_get_contents($path));
44
            $this->recipes[$recipePath] = $recipe;
45
        }
46
    }
47
48
    public function gen(string $destination): ?string
49
    {
50
        foreach ($this->recipes as $recipe) {
51
            // $find will try to return DocConfig for a given config $name.
52
            $findConfig = function (string $name) use ($recipe): ?DocConfig {
53
                if (array_key_exists($name, $recipe->config)) {
54
                    return $recipe->config[$name];
55
                }
56
                foreach ($recipe->require as $r) {
57
                    if (array_key_exists($r, $this->recipes)) {
58
                        if (array_key_exists($name, $this->recipes[$r]->config)) {
59
                            return $this->recipes[$r]->config[$name];
60
                        }
61
                    }
62
                }
63
                foreach ($this->recipes as $r) {
64
                    if (array_key_exists($name, $r->config)) {
65
                        return $r->config[$name];
66
                    }
67
                }
68
                return null;
69
            };
70
            $findConfigOverride = function (DocRecipe $recipe, string $name) use (&$findConfigOverride): ?DocConfig {
71
                foreach ($recipe->require as $r) {
72
                    if (array_key_exists($r, $this->recipes)) {
73
                        if (array_key_exists($name, $this->recipes[$r]->config)) {
74
                            return $this->recipes[$r]->config[$name];
75
                        }
76
                    }
77
                }
78
                foreach ($recipe->require as $r) {
79
                    if (array_key_exists($r, $this->recipes)) {
80
                        return $findConfigOverride($this->recipes[$r], $name);
81
                    }
82
                }
83
                return null;
84
            };
85
            // Replace all {{name}} with link to correct config declaration.
86
            $replaceLinks = function (string $comment) use ($findConfig): string {
87
                $output = '';
88
                $code = false;
89
                foreach (explode("\n", $comment) as $i => $line) {
90
                    if (str_starts_with($line, '```') || str_starts_with($line, '~~~')) {
91
                        $code = !$code;
0 ignored issues
show
introduced by
The condition $code is always false.
Loading history...
92
                    }
93
                    if ($code) {
94
                        $output .= $line;
95
                        $output .= "\n";
96
                        continue;
97
                    }
98
                    $output .= preg_replace_callback('#(\{\{(?<name>[\w_:\-/]+)\}\})#', function ($m) use ($findConfig) {
99
                        $name = $m['name'];
100
                        $config = $findConfig($name);
101
                        if ($config !== null) {
102
                            $md = php_to_md($config->recipePath);
103
                            $anchor = anchor($name);
104
                            return "[$name](/docs/$md#$anchor)";
105
                        }
106
                        return "{{" . $name . "}}";
107
                    }, $line);
108
                    $output .= "\n";
109
                }
110
                return $output;
111
            };
112
            $findTask = function (string $name, bool $searchOtherRecipes = true) use ($recipe): ?DocTask {
113
                if (array_key_exists($name, $recipe->tasks)) {
114
                    return $recipe->tasks[$name];
115
                }
116
                foreach ($recipe->require as $r) {
117
                    if (array_key_exists($r, $this->recipes)) {
118
                        if (array_key_exists($name, $this->recipes[$r]->tasks)) {
119
                            return $this->recipes[$r]->tasks[$name];
120
                        }
121
                    }
122
                }
123
                if ($searchOtherRecipes) {
124
                    foreach ($this->recipes as $r) {
125
                        if (array_key_exists($name, $r->tasks)) {
126
                            return $r->tasks[$name];
127
                        }
128
                    }
129
                }
130
                return null;
131
            };
132
133
            $title = join(' ', array_map('ucfirst', explode('_', $recipe->recipeName))) . ' Recipe';
134
            $config = '';
135
            $tasks = '';
136
            $intro = <<<MD
137
                ```php
138
                require '$recipe->recipePath';
139
                ```
140
141
                [Source](/$recipe->recipePath)
142
143
144
                MD;
145
            if (is_framework_recipe($recipe)) {
146
                $brandName = framework_brand_name($recipe->recipeName);
147
                $typeOfProject = preg_match('/^symfony/i', $recipe->recipeName) ? 'Application' : 'Project';
148
                $title = "How to Deploy a $brandName $typeOfProject";
149
150
                $intro .= <<<MARKDOWN
151
                    Deployer is a free and open source deployment tool written in PHP. 
152
                    It helps you to deploy your $brandName application to a server. 
153
                    It is very easy to use and has a lot of features. 
154
155
                    Three main features of Deployer are:
156
                    - **Provisioning** - provision your server for you.
157
                    - **Zero downtime deployment** - deploy your application without a downtime.
158
                    - **Rollbacks** - rollback your application to a previous version, if something goes wrong.
159
160
                    Additionally, Deployer has a lot of other features, like:
161
                    - **Easy to use** - Deployer is very easy to use. It has a simple and intuitive syntax.
162
                    - **Fast** - Deployer is very fast. It uses parallel connections to deploy your application.
163
                    - **Secure** - Deployer uses SSH to connect to your server.
164
                    - **Supports all major PHP frameworks** - Deployer supports all major PHP frameworks.
165
166
                    You can read more about Deployer in [Getting Started](/docs/getting-started.md).
167
168
169
                    MARKDOWN;
170
171
                $map = function (DocTask $task, $ident = '') use (&$map, $findTask, &$intro): void {
172
                    foreach ($task->group as $taskName) {
173
                        $t = $findTask($taskName);
174
                        if ($t !== null) {
175
                            $intro .= "$ident* {$t->mdLink()} – $t->desc\n";
176
                            if ($t->group !== null) {
177
                                $map($t, $ident . '  ');
178
                            }
179
                        }
180
                    }
181
                };
182
                $deployTask = $findTask('deploy');
183
                if ($deployTask !== null) {
184
                    $intro .= "The [deploy](#deploy) task of **$brandName** consists of:\n";
185
                    $map($deployTask);
186
                }
187
188
                $intro .= "\n\n";
189
190
                $artifactBuildTask = $findTask('artifact:build', false);
191
                $artifactDeployTask = $findTask('artifact:deploy', false);
192
                if ($artifactDeployTask !== null && $artifactBuildTask !== null) {
193
                    $intro .= "In addition the **$brandName** recipe contains an artifact deployment.\n";
194
                    $intro .= <<<MD
195
                        This is a two step process where you first execute
196
197
                        ```php
198
                        bin/dep artifact:build [options] [localhost]
199
                        ```
200
201
                        to build an artifact, which then is deployed on a server with
202
203
                        ```php
204
                        bin/dep artifact:deploy [host]
205
                        ```
206
207
                        The `localhost` to build the artifact on has to be declared local, so either add
208
                        ```php
209
                        localhost()
210
                            ->set('local', true);
211
                        ```
212
                        to your deploy.php or
213
                        ```yaml
214
                        hosts:
215
                            localhost:
216
                                local: true
217
                        ```
218
                        to your deploy yaml.
219
220
                        The [artifact:build](#artifact:build) command of **$brandName** consists of: 
221
                        MD;
222
                    $map($artifactBuildTask);
223
224
                    $intro .= "\n\n The [artifact:deploy](#artifact:deploy) command of **$brandName** consists of:\n";
225
226
                    $map($artifactDeployTask);
227
228
                    $intro .= "\n\n";
229
                }
230
            }
231
            if (count($recipe->require) > 0) {
232
                if (is_framework_recipe($recipe)) {
233
                    $link = recipe_to_md_link($recipe->require[0]);
234
                    $intro .= "The $recipe->recipeName recipe is based on the $link recipe.\n";
235
                } else {
236
                    $intro .= "* Requires\n";
237
                    foreach ($recipe->require as $r) {
238
                        $link = recipe_to_md_link($r);
239
                        $intro .= "  * {$link}\n";
240
                    }
241
                }
242
            }
243
            if (!empty($recipe->comment)) {
244
                $intro .= "\n$recipe->comment\n";
245
            }
246
            if (count($recipe->config) > 0) {
247
                $config .= "## Configuration\n";
248
                foreach ($recipe->config as $c) {
249
                    $config .= "### {$c->name}\n";
250
                    $config .= "[Source](https://github.com/deployphp/deployer/blob/master/{$c->recipePath}#L{$c->lineNumber})\n\n";
251
                    $o = $findConfigOverride($recipe, $c->name);
252
                    if ($o !== null) {
253
                        $md = php_to_md($o->recipePath);
254
                        $anchor = anchor($c->name);
255
                        $config .= "Overrides [{$c->name}](/docs/$md#$anchor) from `$o->recipePath`.\n\n";
256
                    }
257
                    $config .= $replaceLinks($c->comment);
258
                    $config .= "\n";
259
                    if (
260
                        !empty($c->defaultValue)
261
                        && $c->defaultValue !== "''"
262
                        && $c->defaultValue !== '[]'
263
                    ) {
264
                        $config .= "```php title=\"Default value\"\n";
265
                        $config .= $c->defaultValue;
266
                        $config .= "\n";
267
                        $config .= "```\n";
268
                    }
269
                    $config .= "\n\n";
270
                }
271
            }
272
            if (count($recipe->tasks) > 0) {
273
                $tasks .= "## Tasks\n\n";
274
                foreach ($recipe->tasks as $t) {
275
                    $anchorTag = '{#' . anchor($t->name) . '}';
276
                    $name = title($t->name);
277
                    $tasks .= "### $name $anchorTag\n";
278
                    $tasks .= "[Source](https://github.com/deployphp/deployer/blob/master/{$t->recipePath}#L{$t->lineNumber})\n\n";
279
                    $tasks .= add_tailing_dot($t->desc) . "\n\n";
280
                    $tasks .= $replaceLinks($t->comment);
281
                    if (is_array($t->group)) {
282
                        $tasks .= "\n\n";
283
                        $tasks .= "This task is group task which contains next tasks:\n";
284
                        foreach ($t->group as $taskName) {
285
                            $t = $findTask($taskName);
286
                            if ($t !== null) {
287
                                $tasks .= "* {$t->mdLink()}\n";
288
                            } else {
289
                                $tasks .= "* `$taskName`\n";
290
                            }
291
                        }
292
                    }
293
                    $tasks .= "\n\n";
294
                }
295
            }
296
297
            $output = <<<MD
298
                <!-- DO NOT EDIT THIS FILE! -->
299
                <!-- Instead edit $recipe->recipePath -->
300
                <!-- Then run bin/docgen -->
301
302
                # $title
303
304
                $intro
305
                $config
306
                $tasks
307
                MD;
308
309
            $filePath = "$destination/" . php_to_md($recipe->recipePath);
310
            if (!file_exists(dirname($filePath))) {
311
                mkdir(dirname($filePath), 0o755, true);
312
            }
313
            $output = remove_text_emoji($output);
314
            file_put_contents($filePath, $output);
315
        }
316
        $this->generateRecipesIndex($destination);
317
        $this->generateContribIndex($destination);
318
        return null;
319
    }
320
321
    public function generateRecipesIndex(string $destination)
322
    {
323
        $index = "# All Recipes\n\n";
324
        $list = [];
325
        foreach ($this->recipes as $recipe) {
326
            if (preg_match('/^recipe\/[^\/]+\.php$/', $recipe->recipePath)) {
327
                $name = framework_brand_name($recipe->recipeName);
328
                $list[] = "* [$name Recipe](/docs/recipe/{$recipe->recipeName}.md)";
329
            }
330
        }
331
        sort($list);
332
        $index .= implode("\n", $list);
333
        file_put_contents("$destination/recipe/README.md", $index);
334
    }
335
336
    public function generateContribIndex(string $destination)
337
    {
338
        $index = "# All Contrib Recipes\n\n";
339
        $list = [];
340
        foreach ($this->recipes as $recipe) {
341
            if (preg_match('/^contrib\/[^\/]+\.php$/', $recipe->recipePath)) {
342
                $name = ucfirst($recipe->recipeName);
343
                $list[] = "* [$name Recipe](/docs/contrib/$recipe->recipeName.md)";
344
            }
345
        }
346
        sort($list);
347
        $index .= implode("\n", $list);
348
        file_put_contents("$destination/contrib/README.md", $index);
349
    }
350
}
351
352
function trim_comment(string $line): string
353
{
354
    return preg_replace('#^(/\*\*?\s?|\s\*\s?|//\s?)#', '', $line);
355
}
356
357
function indent(string $text): string
358
{
359
    return implode("\n", array_map(function ($line) {
360
        return "  " . $line;
361
    }, explode("\n", $text)));
362
}
363
364
function php_to_md(string $file): string
365
{
366
    return preg_replace('#\.php$#', '.md', $file);
367
}
368
369
function title(string $s): string
370
{
371
    return str_replace(':', '\\:', $s);
372
}
373
374
function anchor(string $s): string
375
{
376
    return strtolower(str_replace(':', '-', $s));
377
}
378
379
function remove_text_emoji(string $text): string
380
{
381
    return preg_replace('/:(bowtie|smile|laughing|blush|smiley|relaxed|smirk|heart_eyes|kissing_heart|kissing_closed_eyes|flushed|relieved|satisfied|grin|wink|stuck_out_tongue_winking_eye|stuck_out_tongue_closed_eyes|grinning|kissing|kissing_smiling_eyes|stuck_out_tongue|sleeping|worried|frowning|anguished|open_mouth|grimacing|confused|hushed|expressionless|unamused|sweat_smile|sweat|disappointed_relieved|weary|pensive|disappointed|confounded|fearful|cold_sweat|persevere|cry|sob|joy|astonished|scream|neckbeard|tired_face|angry|rage|triumph|sleepy|yum|mask|sunglasses|dizzy_face|imp|smiling_imp|neutral_face|no_mouth|innocent|alien|yellow_heart|blue_heart|purple_heart|heart|green_heart|broken_heart|heartbeat|heartpulse|two_hearts|revolving_hearts|cupid|sparkling_heart|sparkles|star|star2|dizzy|boom|collision|anger|exclamation|question|grey_exclamation|grey_question|zzz|dash|sweat_drops|notes|musical_note|fire|hankey|poop|shit|\+1|thumbsup|\-1|thumbsdown|ok_hand|punch|facepunch|fist|v|wave|hand|raised_hand|open_hands|point_up|point_down|point_left|point_right|raised_hands|pray|point_up_2|clap|muscle|metal|fu|walking|runner|running|couple|family|two_men_holding_hands|two_women_holding_hands|dancer|dancers|ok_woman|no_good|information_desk_person|raising_hand|bride_with_veil|person_with_pouting_face|person_frowning|bow|couplekiss|couple_with_heart|massage|haircut|nail_care|boy|girl|woman|man|baby|older_woman|older_man|person_with_blond_hair|man_with_gua_pi_mao|man_with_turban|construction_worker|cop|angel|princess|smiley_cat|smile_cat|heart_eyes_cat|kissing_cat|smirk_cat|scream_cat|crying_cat_face|joy_cat|pouting_cat|japanese_ogre|japanese_goblin|see_no_evil|hear_no_evil|speak_no_evil|guardsman|skull|feet|lips|kiss|droplet|ear|eyes|nose|tongue|love_letter|bust_in_silhouette|busts_in_silhouette|speech_balloon|thought_balloon|feelsgood|finnadie|goberserk|godmode|hurtrealbad|rage1|rage2|rage3|rage4|suspect|trollface|sunny|umbrella|cloud|snowflake|snowman|zap|cyclone|foggy|ocean|cat|dog|mouse|hamster|rabbit|wolf|frog|tiger|koala|bear|pig|pig_nose|cow|boar|monkey_face|monkey|horse|racehorse|camel|sheep|elephant|panda_face|snake|bird|baby_chick|hatched_chick|hatching_chick|chicken|penguin|turtle|bug|honeybee|ant|beetle|snail|octopus|tropical_fish|fish|whale|whale2|dolphin|cow2|ram|rat|water_buffalo|tiger2|rabbit2|dragon|goat|rooster|dog2|pig2|mouse2|ox|dragon_face|blowfish|crocodile|dromedary_camel|leopard|cat2|poodle|paw_prints|bouquet|cherry_blossom|tulip|four_leaf_clover|rose|sunflower|hibiscus|maple_leaf|leaves|fallen_leaf|herb|mushroom|cactus|palm_tree|evergreen_tree|deciduous_tree|chestnut|seedling|blossom|ear_of_rice|shell|globe_with_meridians|sun_with_face|full_moon_with_face|new_moon_with_face|new_moon|waxing_crescent_moon|first_quarter_moon|waxing_gibbous_moon|full_moon|waning_gibbous_moon|last_quarter_moon|waning_crescent_moon|last_quarter_moon_with_face|first_quarter_moon_with_face|moon|earth_africa|earth_americas|earth_asia|volcano|milky_way|partly_sunny|octocat|squirrel|bamboo|gift_heart|dolls|school_satchel|mortar_board|flags|fireworks|sparkler|wind_chime|rice_scene|jack_o_lantern|ghost|santa|christmas_tree|gift|bell|no_bell|tanabata_tree|tada|confetti_ball|balloon|crystal_ball|cd|dvd|floppy_disk|camera|video_camera|movie_camera|computer|tv|iphone|phone|telephone|telephone_receiver|pager|fax|minidisc|vhs|sound|speaker|mute|loudspeaker|mega|hourglass|hourglass_flowing_sand|alarm_clock|watch|radio|satellite|loop|mag|mag_right|unlock|lock|lock_with_ink_pen|closed_lock_with_key|key|bulb|flashlight|high_brightness|low_brightness|electric_plug|battery|calling|email|mailbox|postbox|bath|bathtub|shower|toilet|wrench|nut_and_bolt|hammer|seat|moneybag|yen|dollar|pound|euro|credit_card|money_with_wings|e-mail|inbox_tray|outbox_tray|envelope|incoming_envelope|postal_horn|mailbox_closed|mailbox_with_mail|mailbox_with_no_mail|door|smoking|bomb|gun|hocho|pill|syringe|page_facing_up|page_with_curl|bookmark_tabs|bar_chart|chart_with_upwards_trend|chart_with_downwards_trend|scroll|clipboard|calendar|date|card_index|file_folder|open_file_folder|scissors|pushpin|paperclip|black_nib|pencil2|straight_ruler|triangular_ruler|closed_book|green_book|blue_book|orange_book|notebook|notebook_with_decorative_cover|ledger|books|bookmark|name_badge|microscope|telescope|newspaper|football|basketball|soccer|baseball|tennis|8ball|rugby_football|bowling|golf|mountain_bicyclist|bicyclist|horse_racing|snowboarder|swimmer|surfer|ski|spades|hearts|clubs|diamonds|gem|ring|trophy|musical_score|musical_keyboard|violin|space_invader|video_game|black_joker|flower_playing_cards|game_die|dart|mahjong|clapper|memo|pencil|book|art|microphone|headphones|trumpet|saxophone|guitar|shoe|sandal|high_heel|lipstick|boot|shirt|tshirt|necktie|womans_clothes|dress|running_shirt_with_sash|jeans|kimono|bikini|ribbon|tophat|crown|womans_hat|mans_shoe|closed_umbrella|briefcase|handbag|pouch|purse|eyeglasses|fishing_pole_and_fish|coffee|tea|sake|baby_bottle|beer|beers|cocktail|tropical_drink|wine_glass|fork_and_knife|pizza|hamburger|fries|poultry_leg|meat_on_bone|spaghetti|curry|fried_shrimp|bento|sushi|fish_cake|rice_ball|rice_cracker|rice|ramen|stew|oden|dango|egg|bread|doughnut|custard|icecream|ice_cream|shaved_ice|birthday|cake|cookie|chocolate_bar|candy|lollipop|honey_pot|apple|green_apple|tangerine|lemon|cherries|grapes|watermelon|strawberry|peach|melon|banana|pear|pineapple|sweet_potato|eggplant|tomato|corn|house|house_with_garden|school|office|post_office|hospital|bank|convenience_store|love_hotel|hotel|wedding|church|department_store|european_post_office|city_sunrise|city_sunset|japanese_castle|european_castle|tent|factory|tokyo_tower|japan|mount_fuji|sunrise_over_mountains|sunrise|stars|statue_of_liberty|bridge_at_night|carousel_horse|rainbow|ferris_wheel|fountain|roller_coaster|ship|speedboat|boat|sailboat|rowboat|anchor|rocket|airplane|helicopter|steam_locomotive|tram|mountain_railway|bike|aerial_tramway|suspension_railway|mountain_cableway|tractor|blue_car|oncoming_automobile|car|red_car|taxi|oncoming_taxi|articulated_lorry|bus|oncoming_bus|rotating_light|police_car|oncoming_police_car|fire_engine|ambulance|minibus|truck|train|station|train2|bullettrain_front|bullettrain_side|light_rail|monorail|railway_car|trolleybus|ticket|fuelpump|vertical_traffic_light|traffic_light|warning|construction|beginner|atm|slot_machine|busstop|barber|hotsprings|checkered_flag|crossed_flags|izakaya_lantern|moyai|circus_tent|performing_arts|round_pushpin|triangular_flag_on_post|jp|kr|cn|us|fr|es|it|ru|gb|uk|de|one|two|three|four|five|six|seven|eight|nine|keycap_ten|1234|zero|hash|symbols|arrow_backward|arrow_down|arrow_forward|arrow_left|capital_abcd|abcd|abc|arrow_lower_left|arrow_lower_right|arrow_right|arrow_up|arrow_upper_left|arrow_upper_right|arrow_double_down|arrow_double_up|arrow_down_small|arrow_heading_down|arrow_heading_up|leftwards_arrow_with_hook|arrow_right_hook|left_right_arrow|arrow_up_down|arrow_up_small|arrows_clockwise|arrows_counterclockwise|rewind|fast_forward|information_source|ok|twisted_rightwards_arrows|repeat|repeat_one|new|top|up|cool|free|ng|cinema|koko|signal_strength|u5272|u5408|u55b6|u6307|u6708|u6709|u6e80|u7121|u7533|u7a7a|u7981|sa|restroom|mens|womens|baby_symbol|no_smoking|parking|wheelchair|metro|baggage_claim|accept|wc|potable_water|put_litter_in_its_place|secret|congratulations|m|passport_control|left_luggage|customs|ideograph_advantage|cl|sos|id|no_entry_sign|underage|no_mobile_phones|do_not_litter|non-potable_water|no_bicycles|no_pedestrians|children_crossing|no_entry|eight_spoked_asterisk|eight_pointed_black_star|heart_decoration|vs|vibration_mode|mobile_phone_off|chart|currency_exchange|aries|taurus|gemini|cancer|leo|virgo|libra|scorpius|sagittarius|capricorn|aquarius|pisces|ophiuchus|six_pointed_star|negative_squared_cross_mark|a|b|ab|o2|diamond_shape_with_a_dot_inside|recycle|end|on|soon|clock1|clock130|clock10|clock1030|clock11|clock1130|clock12|clock1230|clock2|clock230|clock3|clock330|clock4|clock430|clock5|clock530|clock6|clock630|clock7|clock730|clock8|clock830|clock9|clock930|heavy_dollar_sign|copyright|registered|tm|x|heavy_exclamation_mark|bangbang|interrobang|o|heavy_multiplication_x|heavy_plus_sign|heavy_minus_sign|heavy_division_sign|white_flower|100|heavy_check_mark|ballot_box_with_check|radio_button|link|curly_loop|wavy_dash|part_alternation_mark|trident|black_square|white_square|white_check_mark|black_square_button|white_square_button|black_circle|white_circle|red_circle|large_blue_circle|large_blue_diamond|large_orange_diamond|small_blue_diamond|small_orange_diamond|small_red_triangle|small_red_triangle_down|shipit):/i', ':&#8203;\1:', $text);
382
}
383
384
function add_tailing_dot(string $sentence): string
385
{
386
    if (empty($sentence)) {
387
        return $sentence;
388
    }
389
    if (str_ends_with($sentence, '.')) {
390
        return $sentence;
391
    }
392
    return $sentence . '.';
393
}
394
395
function recipe_to_md_link(string $recipe): string
396
{
397
    $md = php_to_md($recipe);
398
    $basename = basename($recipe, '.php');
399
    return "[$basename](/docs/$md)";
400
}
401
402
function is_framework_recipe(DocRecipe $recipe): bool
403
{
404
    return preg_match('/recipe\/[\w_\d]+\.php$/', $recipe->recipePath) &&
405
    !in_array($recipe->recipeName, ['common', 'composer', 'provision'], true);
406
}
407
408
function framework_brand_name(string $brandName): string
409
{
410
    $brandName = preg_replace('/(\w+)(\d)/', '$1 $2', $brandName);
411
    $brandName = preg_replace('/typo 3/', 'TYPO3', $brandName);
412
    $brandName = preg_replace('/yii/', 'Yii2', $brandName);
413
    $brandName = preg_replace('/wordpress/', 'WordPress', $brandName);
414
    $brandName = preg_replace('/_/', ' ', $brandName);
415
    $brandName = preg_replace('/framework/', 'Framework', $brandName);
416
    return ucfirst($brandName);
417
}
418