Passed
Push — master ( 203740...2c4c0a )
by Kacper
04:22
created

Language::__set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 2
crap 1
1
<?php
2
/**
3
 * Highlighter
4
 *
5
 * Copyright (C) 2015, Some right reserved.
6
 *
7
 * @author Kacper "Kadet" Donat <[email protected]>
8
 *
9
 * Contact with author:
10
 * Xmpp: [email protected]
11
 * E-mail: [email protected]
12
 *
13
 * From Kadet with love.
14
 */
15
16
namespace Kadet\Highlighter\Language;
17
18
use Kadet\Highlighter\Matcher\WholeMatcher;
19
use Kadet\Highlighter\Parser\LanguageToken;
20
use Kadet\Highlighter\Parser\Rule;
21
use Kadet\Highlighter\Parser\Token;
22
use Kadet\Highlighter\Parser\TokenFactory;
23
use Kadet\Highlighter\Parser\TokenIterator;
24
use Kadet\Highlighter\Parser\TokenList;
25
use Kadet\Highlighter\Utils\ArrayHelper;
26
27
/**
28
 * Class Language
29
 *
30
 * @package Kadet\Highlighter\Language
31
 */
32
abstract class Language
33
{
34
    /**
35
     * @var array
36
     */
37
    protected $_options = [];
38
    /**
39
     * Tokenizer rules
40
     *
41
     * @var Rule[]
42
     */
43
    private $_rules;
44
45
    /**
46
     * Language constructor.
47
     *
48
     * @param array $options
49
     */
50 22
    public function __construct(array $options = [])
51
    {
52 22
        $this->_options = array_merge(
53
            [
54 22
                'embedded' => [],
55 22
            ], $this->_options, $options
56 22
        );
57
58 22
        $this->_rules = $this->getRules();
59 22
    }
60
61
    /**
62
     * Tokenization rules definition
63
     *
64
     * @return array
65
     */
66
    abstract public function getRules();
67
68
    /**
69
     * Parses source and removes wrong tokens.
70
     *
71
     * @param TokenIterator|string $tokens
72
     *
73
     * @param array                $additional
74
     * @param bool                 $embedded
75
     *
76
     * @return TokenIterator
77
     */
78 12
    public function parse($tokens = null, $additional = [], $embedded = false)
79
    {
80 12
        if (is_string($tokens)) {
81 11
            $tokens = $this->tokenize($tokens, $additional, $embedded);
82 12
        } elseif (!$tokens instanceof TokenIterator) {
83
            // Todo: Own Exceptions
84 1
            throw new \InvalidArgumentException('$tokens must be string or TokenIterator');
85
        }
86
87 11
        $start = $tokens->current();
88 11
        $context = [];
89
90
        /** @var Token[] $result */
91 11
        $result = [$start];
92
        /** @var Token[] $all */
93 11
        $all = [];
94
95
        /** @var Token $token */
96 11
        for ($tokens->next(); $tokens->valid(); $tokens->next()) {
97 11
            $token = $tokens->current();
98
99 11
            if (!$token->isValid($this, $context)) {
100 3
                continue;
101
            }
102
103 11
            if ($token->isStart()) {
104 10
                if ($token instanceof LanguageToken) {
105
                    /** @var LanguageToken $token */
106 2
                    $result = array_merge(
107 2
                        $result,
108 2
                        $token->getInjected()->parse($tokens)->getTokens()
109 2
                    );
110 2
                } else {
111 10
                    $all[spl_object_hash($token)] = $result[] = $token;
112 10
                    $context[spl_object_hash($token)] = $token->name;
113
                }
114 10
            } else {
115 12
                $start = $token->getStart();
116
117
                /** @noinspection PhpUndefinedMethodInspection bug */
118 11
                if ($token instanceof LanguageToken && $token->getLanguage() === $this) {
119 11
                    $result[0]->setEnd($token);
120
121 11
                    if ($result[0]->getRule()->postProcess) {
122 1
                        $source = substr($tokens->getSource(), $result[0]->pos, $result[0]->getLength());
123
124 1
                        $tokens = $this->tokenize($source, $result, $result[0]->pos, true);
125 1
                        $result = $this->parse($tokens)->getTokens();
126 1
                    }
127
128
                    # closing unclosed tokens
129 11
                    foreach (array_reverse($context) as $hash => $name) {
130 1
                        $end = new Token([$name, 'pos' => $token->pos]);
131 1
                        $all[$hash]->setEnd($end);
132 1
                        $result[] = $end;
133 11
                    }
134
135 11
                    $result[] = $token;
136 11
                    break;
137
                } else {
138 9
                    if ($start) {
139 8
                        unset($context[spl_object_hash($start)]);
140 8
                    } else {
141
                        /** @noinspection PhpUnusedParameterInspection */
142 1
                        $start = ArrayHelper::find(
143 1
                            array_reverse($context), function ($k, $v) use ($token) {
144 1
                            return $v === $token->name;
145 1
                        });
146
147 1
                        if ($start !== false) {
148 1
                            $token->setStart($all[$start]);
149 1
                            unset($context[$start]);
150 1
                        }
151
                    }
152
153 9
                    $result[] = $token;
154
                }
155
            }
156 10
        }
157
158 11
        return new TokenIterator($result, $tokens->getSource());
159
    }
160
161 12
    public function tokenize($source, $additional = [], $offset = 0, $embedded = false)
162
    {
163 12
        $iterator = new TokenIterator(
164 12
            $this->_tokens($source, $offset, $additional, $embedded)->sort()->toArray(), $source
165 12
        );
166
167 12
        return $iterator;
168
    }
169
170
    /**
171
     * Tokenize source
172
     *
173
     * @param       $source
174
     *
175
     * @param int   $offset
176
     * @param array $additional
177
     *
178
     * @param bool  $embedded
179
     *
180
     * @return TokenList
181
     */
182 12
    private function _tokens($source, $offset = 0, $additional = [], $embedded = false)
183
    {
184 12
        $result = new TokenList();
185
186
        /** @var Language $language */
187 12
        foreach ($this->_rules($embedded) as $rule) {
188 12
            $rule->factory->setOffset($offset);
189 12
            foreach ($rule->match($source) as $token) {
190 12
                $result->add($token);
191 12
            }
192 12
        }
193
194 12
        return $result->batch($additional);
195
    }
196
197
    /**
198
     * @param bool $embedded
199
     *
200
     * @return Rule[]
201
     */
202 12
    private function _rules($embedded = false)
203
    {
204 12
        $all = $this->_rules;
205 12
        if (!$embedded) {
206 12
            $all['language.' . $this->getIdentifier()] = $this->getOpenClose();
207 12
        }
208
209
        // why this code sucks so much? Because RecursiveIterator performance such a lot more.
210 12
        foreach ($all as $name => $rules) {
211 12
            if (!is_array($rules)) {
212 12
                $rules = [$rules];
213 12
            }
214
215
            /** @var Rule $rule */
216 12
            foreach ($rules as $rule) {
217 12
                if ($rule->language === false) {
218 11
                    $rule->language = $this;
219 11
                }
220
221 12
                $rule->factory->setBase($name);
222
223 12
                yield $rule;
224 12
            }
225 12
        }
226
227 12
        foreach ($this->getEmbedded() as $language) {
228 1
            foreach ($language->_rules() as $rule) {
229 1
                yield $rule;
230 1
            }
231 12
        }
232 12
    }
233
234
    /**
235
     * Unique language identifier, for example 'php'
236
     *
237
     * @return string
238
     */
239
    abstract public function getIdentifier();
240
241
    /**
242
     * Language range Rule(s)
243
     *
244
     * @return Rule|Rule[]
245
     */
246 16
    public function getOpenClose()
247
    {
248 16
        return new Rule(
249 16
            new WholeMatcher(), [
250 16
                'priority' => 1000,
251 16
                'factory'  => new TokenFactory(LanguageToken::class),
252 16
                'inject'   => $this,
253 16
                'language' => null,
254 16
                'context'  => ['!!'],
255
            ]
256 16
        );
257
    }
258
259
    /**
260
     * @return Language[]
261
     */
262 13
    public function getEmbedded()
263
    {
264 13
        return $this->_options['embedded'];
265
    }
266
267
    /**
268
     * @param Language $lang
269
     */
270 1
    public function embed(Language $lang)
271
    {
272 1
        $this->_options['embedded'][] = $lang;
273 1
    }
274
275 1
    public function __get($name)
276
    {
277 1
        return isset($this->_options[$name]) ? $this->_options[$name] : null;
278
    }
279
280 1
    public function __set($name, $value)
281
    {
282 1
        $this->_options[$name] = $value;
283 1
    }
284
}
285