Passed
Push — master ( d3eb9e...ba3bb3 )
by Sebastian
03:58
created

Mailcode_Parser::replaceBrackets()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 2
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * File containing the {@see Mailcode_Parser} class.
4
 * 
5
 * @package Mailcode
6
 * @subpackage Parser
7
 * @see Mailcode_Parser
8
 */
9
10
declare(strict_types=1);
11
12
namespace Mailcode;
13
14
use AppUtils\ConvertHelper;
15
16
/**
17
 * Mailcode parser, capable of detecting commands in strings.
18
 * 
19
 * @package Mailcode
20
 * @subpackage Parser
21
 * @author Sebastian Mordziol <[email protected]>
22
 */
23
class Mailcode_Parser
24
{
25
    public const ERROR_NOT_A_COMMAND = 73301;
26
27
    public const COMMAND_REGEX_PARTS = array( 
28
        '{\s*([a-z]+)\s*}',
29
        '{\s*([a-z]+)\s*:([^}]*)}',
30
        '{\s*([a-z]+)\s+([a-z-]+)\s*:([^}]*)}'
31
    );
32
    
33
   /**
34
    * @var Mailcode
35
    */
36
    protected $mailcode;
37
    
38
   /**
39
    * @var Mailcode_Commands
40
    */
41
    protected $commands;
42
43
    /**
44
     * @var Mailcode_Commands_Command_Type_Opening[]
45
     */
46
    protected $stack = array();
47
48
    public function __construct(Mailcode $mailcode)
49
    {
50
        $this->mailcode = $mailcode;
51
        $this->commands = $this->mailcode->getCommands();
52
    }
53
    
54
   /**
55
    * Gets the regex format string used to detect commands.
56
    * 
57
    * @return string
58
    */
59
    protected static function getRegex() : string
60
    {
61
        return '/'.implode('|', self::COMMAND_REGEX_PARTS).'/sixU';
62
    }
63
    
64
   /**
65
    * Parses a string to detect all commands within. Returns a
66
    * collection instance that contains information on all the 
67
    * commands.
68
    * 
69
    * @param string $string
70
    * @return Mailcode_Collection A collection with all unique commands found.
71
    */
72
    public function parseString(string $string) : Mailcode_Collection
73
    {
74
        $collection = new Mailcode_Collection();
75
        
76
        $string = $this->prepareString($string);
77
        
78
        $matches = array();
79
        preg_match_all(self::getRegex(), $string, $matches, PREG_PATTERN_ORDER);
80
        
81
        $total = count($matches[0]);
82
        
83
        for($i=0; $i < $total; $i++)
84
        {
85
            $match = $this->parseMatch($matches, $i);
86
            
87
            $this->processMatch($match, $collection);
88
        }
89
90
        $collection->finalize();
91
92
        return $collection;
93
    }
94
95
    public const LITERAL_BRACKET_LEFT_REPLACEMENT = '﴾';
96
    public const LITERAL_BRACKET_RIGHT_REPLACEMENT = '﴿';
97
98
    protected function prepareString(string $subject) : string
99
    {
100
        preg_match_all('/"[^"]+"/U', $subject, $result, PREG_PATTERN_ORDER);
101
102
        $matches = array_unique($result[0]);
103
104
        if(!empty($matches))
105
        {
106
            foreach($matches as $match)
107
            {
108
                if(strpos($match, '{') !== false || strpos($match, '}') !== false)
109
                {
110
                    $subject = $this->replaceBrackets($subject, $match);
111
                }
112
            }
113
        }
114
115
        if(!ConvertHelper::isStringHTML($subject))
116
        {
117
            return $subject;
118
        }
119
120
        // remove all <style> tags to avoid conflicts with CSS code
121
        return preg_replace('%<style\b[^>]*>(.*?)</style>%six', '', $subject);
122
    }
123
124
    private function replaceBrackets(string $subject, string $needle) : string
125
    {
126
        $replacement =  str_replace(
127
            array('{', '}'),
128
            array(
129
                self::LITERAL_BRACKET_LEFT_REPLACEMENT,
130
                self::LITERAL_BRACKET_RIGHT_REPLACEMENT
131
            ),
132
            $needle
133
        );
134
135
        return str_replace($needle, $replacement, $subject);
136
    }
137
138
   /**
139
    * Processes a single match found in the string: creates the command,
140
    * and adds it to the collection if it's a valid command, or to the list
141
    * of invalid commands otherwise.
142
    * 
143
    * @param Mailcode_Parser_Match $match
144
    * @param Mailcode_Collection $collection
145
    */
146
    protected function processMatch(Mailcode_Parser_Match $match, Mailcode_Collection $collection) : void
147
    {
148
        $name = $match->getName();
149
150
        if (!$this->commands->nameExists($name)) {
151
            $collection->addErrorMessage(
152
                $match->getMatchedString(),
153
                t('No command found with the name %1$s.', $name),
154
                Mailcode_Commands_Command::VALIDATION_UNKNOWN_COMMAND_NAME
155
            );
156
            return;
157
        }
158
159
        $cmd = $this->commands->createCommand(
160
            $this->commands->getIDByName($name),
161
            $match->getType(),
162
            $match->getParams(),
163
            $match->getMatchedString()
164
        );
165
166
        $this->handleNesting($cmd);
167
168
        if (!$cmd->isValid())
169
        {
170
            $collection->addInvalidCommand($cmd);
171
            return;
172
        }
173
174
        $collection->addCommand($cmd);
175
    }
176
177
    private function handleNesting(Mailcode_Commands_Command $cmd) : void
178
    {
179
        // Set the command's parent from the stack, if any is present.
180
        if(!empty($this->stack))
181
        {
182
            $cmd->setParent($this->getStackLast());
183
        }
184
185
        // Handle opening and closing commands, adding and removing from the stack.
186
        if($cmd instanceof Mailcode_Commands_Command_Type_Opening)
187
        {
188
            $this->stack[] = $cmd;
189
        }
190
        else if($cmd instanceof Mailcode_Commands_Command_Type_Closing)
191
        {
192
            array_pop($this->stack);
193
        }
194
    }
195
196
    private function getStackLast() : Mailcode_Commands_Command
197
    {
198
        $cmd = $this->stack[array_key_last($this->stack)];
199
200
        if($cmd instanceof Mailcode_Commands_Command)
201
        {
202
            return $cmd;
203
        }
204
205
        throw new Mailcode_Exception('Not a command', '', self::ERROR_NOT_A_COMMAND);
206
    }
207
    
208
   /**
209
    * Parses a single regex match: determines which named group
210
    * matches, and retrieves the according information.
211
    * 
212
    * @param array[] $matches The regex results array.
213
    * @param int $index The matched index.
214
    * @return Mailcode_Parser_Match
215
    */
216
    protected function parseMatch(array $matches, int $index) : Mailcode_Parser_Match
217
    {
218
        $name = ''; // the command name, e.g. "showvar"
219
        $type = '';
220
        $params = '';
221
        
222
        // 1 = single command
223
        // 2 = parameter command, name
224
        // 3 = parameter command, params
225
        // 4 = parameter type command, name
226
        // 5 = parameter type command, type
227
        // 6 = parameter type command, params
228
        
229
        if(!empty($matches[1][$index]))
230
        {
231
            $name = $matches[1][$index];
232
        }
233
        else if(!empty($matches[2][$index]))
234
        {
235
            $name = $matches[2][$index];
236
            $params = $matches[3][$index];
237
        }
238
        else if(!empty($matches[4][$index]))
239
        {
240
            $name = $matches[4][$index];
241
            $type = $matches[5][$index];
242
            $params = $matches[6][$index];
243
        }
244
        
245
        return new Mailcode_Parser_Match(
246
            $name, 
247
            $type, 
248
            $params, 
249
            $matches[0][$index]
250
        );
251
    }
252
    
253
   /**
254
    * Creates an instance of the safeguard tool, which
255
    * is used to safeguard commands in a string with placeholders.
256
    * 
257
    * @param string $subject The string to use to safeguard commands in.
258
    * @return Mailcode_Parser_Safeguard
259
    * @see Mailcode_Parser_Safeguard
260
    */
261
    public function createSafeguard(string $subject) : Mailcode_Parser_Safeguard
262
    {
263
        return new Mailcode_Parser_Safeguard($this, $subject);
264
    }
265
266
    /**
267
     * Creates a statement parser, which is used to validate arbitrary
268
     * command statements.
269
     *
270
     * @param string $statement
271
     * @param bool $freeform
272
     * @param Mailcode_Commands_Command|null $sourceCommand
273
     * @return Mailcode_Parser_Statement
274
     */
275
    public function createStatement(string $statement, bool $freeform=false, ?Mailcode_Commands_Command $sourceCommand=null) : Mailcode_Parser_Statement
276
    {
277
        return new Mailcode_Parser_Statement($statement, $freeform, $sourceCommand);
278
    }
279
}
280