Passed
Push — master ( 4523d8...09a038 )
by Caen
03:14 queued 12s
created

BladeMatterParser::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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