Passed
Push — master ( 9253d7...e42fcd )
by Sebastian
03:09
created

Mailcode_Parser::getStackLast()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 10
rs 10
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
    const ERROR_NOT_A_COMMAND = 73301;
26
27
    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
        return $collection;
91
    }
92
    
93
    protected function prepareString(string $subject) : string
94
    {
95
        if(!ConvertHelper::isStringHTML($subject))
96
        {
97
            return $subject;
98
        }
99
100
        // remove all <style> tags to avoid conflicts with CSS code
101
        return preg_replace('%<style\b[^>]*>(.*?)</style>%six', '', $subject);
102
    }
103
    
104
   /**
105
    * Processes a single match found in the string: creates the command,
106
    * and adds it to the collection if it's a valid command, or to the list
107
    * of invalid commands otherwise.
108
    * 
109
    * @param Mailcode_Parser_Match $match
110
    * @param Mailcode_Collection $collection
111
    */
112
    protected function processMatch(Mailcode_Parser_Match $match, Mailcode_Collection $collection) : void
113
    {
114
        $name = $match->getName();
115
116
        if (!$this->commands->nameExists($name)) {
117
            $collection->addErrorMessage(
118
                $match->getMatchedString(),
119
                t('No command found with the name %1$s.', $name),
120
                Mailcode_Commands_Command::VALIDATION_UNKNOWN_COMMAND_NAME
121
            );
122
            return;
123
        }
124
125
        $cmd = $this->commands->createCommand(
126
            $this->commands->getIDByName($name),
127
            $match->getType(),
128
            $match->getParams(),
129
            $match->getMatchedString()
130
        );
131
132
        if (!$cmd->isValid()) {
133
            $collection->addInvalidCommand($cmd);
134
            return;
135
        }
136
137
        $collection->addCommand($cmd);
138
139
        $this->handleNesting($cmd);
140
    }
141
142
    private function handleNesting(Mailcode_Commands_Command $cmd) : void
143
    {
144
        // Set the command's parent from the stack, if any is present.
145
        if(!empty($this->stack))
146
        {
147
            $cmd->setParent($this->getStackLast());
148
        }
149
150
        // Handle opening and closing commands, adding and removing from the stack.
151
        if($cmd instanceof Mailcode_Commands_Command_Type_Opening)
152
        {
153
            $this->stack[] = $cmd;
154
        }
155
        else if($cmd instanceof Mailcode_Commands_Command_Type_Closing)
156
        {
157
            array_pop($this->stack);
158
        }
159
    }
160
161
    private function getStackLast() : Mailcode_Commands_Command
162
    {
163
        $cmd = $this->stack[array_key_last($this->stack)];
164
165
        if($cmd instanceof Mailcode_Commands_Command)
166
        {
167
            return $cmd;
168
        }
169
170
        throw new Mailcode_Exception('Not a command', '', self::ERROR_NOT_A_COMMAND);
171
    }
172
    
173
   /**
174
    * Parses a single regex match: determines which named group
175
    * matches, and retrieves the according information.
176
    * 
177
    * @param array[] $matches The regex results array.
178
    * @param int $index The matched index.
179
    * @return Mailcode_Parser_Match
180
    */
181
    protected function parseMatch(array $matches, int $index) : Mailcode_Parser_Match
182
    {
183
        $name = ''; // the command name, e.g. "showvar"
184
        $type = '';
185
        $params = '';
186
        
187
        // 1 = single command
188
        // 2 = parameter command, name
189
        // 3 = parameter command, params
190
        // 4 = parameter type command, name
191
        // 5 = parameter type command, type
192
        // 6 = parameter type command, params
193
        
194
        if(!empty($matches[1][$index]))
195
        {
196
            $name = $matches[1][$index];
197
        }
198
        else if(!empty($matches[2][$index]))
199
        {
200
            $name = $matches[2][$index];
201
            $params = $matches[3][$index];
202
        }
203
        else if(!empty($matches[4][$index]))
204
        {
205
            $name = $matches[4][$index];
206
            $type = $matches[5][$index];
207
            $params = $matches[6][$index];
208
        }
209
        
210
        return new Mailcode_Parser_Match(
211
            $name, 
212
            $type, 
213
            $params, 
214
            $matches[0][$index]
215
        );
216
    }
217
    
218
   /**
219
    * Creates an instance of the safeguard tool, which
220
    * is used to safeguard commands in a string with placeholders.
221
    * 
222
    * @param string $subject The string to use to safeguard commands in.
223
    * @return Mailcode_Parser_Safeguard
224
    * @see Mailcode_Parser_Safeguard
225
    */
226
    public function createSafeguard(string $subject) : Mailcode_Parser_Safeguard
227
    {
228
        return new Mailcode_Parser_Safeguard($this, $subject);
229
    }
230
    
231
   /**
232
    * Creates a statement parser, which is used to validate arbitrary
233
    * command statements.
234
    * 
235
    * @param string $statement
236
    * @param bool $freeform
237
    * @return Mailcode_Parser_Statement
238
    */
239
    public function createStatement(string $statement, bool $freeform=false) : Mailcode_Parser_Statement
240
    {
241
        return new Mailcode_Parser_Statement($statement, $freeform);
242
    }
243
}
244