Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
11 / 11
CRAP
100.00% covered (success)
100.00%
1 / 1
BladeMatterParser
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
11 / 11
20
100.00% covered (success)
100.00%
1 / 1
 parseFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 lineMatchesFrontMatter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 extractKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 extractValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getValueWithType
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 parseArrayString
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 isValueArrayString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Framework\Actions;
6
7use RuntimeException;
8use Hyde\Facades\Filesystem;
9
10use function str_ends_with;
11use function str_starts_with;
12use function substr_count;
13use function json_decode;
14use function explode;
15use function strlen;
16use function strpos;
17use function substr;
18use function trim;
19
20/**
21 * @experimental Parse the front matter in a Blade file.
22 *
23 * Accepts a string to make it easier to mock when testing.
24 *
25 * @phpstan-consistent-constructor
26 *
27 * === DOCUMENTATION (draft) ===
28 *
29 * ## Front Matter in Markdown
30 *
31 * HydePHP uses a special syntax called BladeMatter that allows you to define variables in a Blade file,
32 * and have Hyde statically parse them into the front matter of the page model. This allows metadata
33 * in your Blade pages to be used when Hyde generates dynamic data like page titles and SEO tags.
34 *
35 * ### Syntax
36 *
37 * Any line following the syntax below will be added to the parsed page object's front matter.
38 *
39 * @example `@php($title = 'BladeMatter Test')`
40 * This would then be parsed into the following array in the page model: ['title' => 'BladeMatter Test']
41 *
42 * ### Limitations
43 * Each directive must be on its own line, and start with `@php($.`. Arrays are currently not supported.
44 */
45class BladeMatterParser
46{
47    protected string $contents;
48    protected array $matter;
49
50    /** @var string The directive signature used to determine if a line should be parsed. */
51    protected const SEARCH = '@php($';
52
53    public static function parseFile(string $path): array
54    {
55        return static::parseString(Filesystem::getContents($path));
56    }
57
58    public static function parseString(string $contents): array
59    {
60        return (new static($contents))->parse()->get();
61    }
62
63    public function __construct(string $contents)
64    {
65        $this->contents = $contents;
66    }
67
68    public function get(): array
69    {
70        return $this->matter;
71    }
72
73    public function parse(): static
74    {
75        $this->matter = [];
76
77        $lines = explode("\n", $this->contents);
78
79        foreach ($lines as $line) {
80            if (static::lineMatchesFrontMatter($line)) {
81                $this->matter[static::extractKey($line)] = static::getValueWithType(static::extractValue($line));
82            }
83        }
84
85        return $this;
86    }
87
88    protected static function lineMatchesFrontMatter(string $line): bool
89    {
90        return str_starts_with($line, static::SEARCH);
91    }
92
93    protected static function extractKey(string $line): string
94    {
95        // Remove search prefix
96        $key = substr($line, strlen(static::SEARCH));
97
98        // Remove everything after the first equals sign
99        $key = substr($key, 0, strpos($key, '='));
100
101        // Return trimmed line
102        return trim($key);
103    }
104
105    protected static function extractValue(string $line): string
106    {
107        // Trim any trailing spaces and newlines
108        $key = trim($line);
109
110        // Remove everything before the first equals sign
111        $key = substr($key, strpos($key, '=') + 1);
112
113        // Remove closing parenthesis
114        $key = substr($key, 0, strlen($key) - 1);
115
116        // Remove any quotes so we can normalize the value
117        $key = trim($key, ' "\'');
118
119        // Return trimmed line
120        return trim($key);
121    }
122
123    /** @return scalar|array<string, scalar> */
124    protected static function getValueWithType(string $value): mixed
125    {
126        $value = trim($value);
127
128        if ($value === 'null') {
129            return null;
130        }
131
132        if (static::isValueArrayString($value)) {
133            return static::parseArrayString($value);
134        }
135
136        // This will cast integers, floats, and booleans to their respective types
137        // Still working on a way to handle multidimensional arrays and objects
138
139        /** @var scalar|null $decoded */
140        $decoded = json_decode($value);
141
142        return $decoded ?? $value;
143    }
144
145    /** @return array<string, scalar> */
146    protected static function parseArrayString(string $string): array
147    {
148        $array = [];
149
150        // Trim input string
151        $string = trim($string);
152
153        // Check if string is an array
154        if (! static::isValueArrayString($string)) {
155            throw new RuntimeException('Failed parsing BladeMatter array. Input string must follow array syntax.');
156        }
157
158        // Check if string is multidimensional (not yet supported)
159        if ((substr_count($string, '[') > 1) || (substr_count($string, ']') > 1)) {
160            throw new RuntimeException('Failed parsing BladeMatter array. Multidimensional arrays are not supported yet.');
161        }
162
163        // Remove opening and closing brackets
164        $string = substr($string, 1, strlen($string) - 2);
165
166        // Tokenize string between commas
167        $tokens = explode(',', $string);
168
169        // Parse each token
170        foreach ($tokens as $token) {
171            // Split string into key/value pairs
172            $pair = explode('=>', $token);
173
174            // Add key/value pair to array
175            $key = (string) static::getValueWithType(trim(trim($pair[0]), "'"));
176            $value = static::getValueWithType(trim(trim($pair[1]), "'"));
177
178            $array[$key] = $value;
179        }
180
181        return $array;
182    }
183
184    protected static function isValueArrayString(string $string): bool
185    {
186        return str_starts_with($string, '[') && str_ends_with($string, ']');
187    }
188}