4
* Our in-house implementation of a parser.
6
* A pure PHP parser, DirectLex has absolutely no dependencies, making
7
* it a reasonably good default for PHP4. Written with efficiency in mind,
8
* it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it
9
* pales in comparison to HTMLPurifier_Lexer_DOMLex.
11
* @todo Reread XML spec and document differences.
13
class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
16
public $tracksLineNumbers = true;
19
* Whitespace characters for str(c)spn.
21
protected $_whitespace = "\x20\x09\x0D\x0A";
24
* Callback function for script CDATA fudge
25
* @param $matches, in form of array(opening tag, contents, closing tag)
27
protected function scriptCallback($matches) {
28
return $matches[1] . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8') . $matches[3];
31
public function tokenizeHTML($html, $config, $context) {
33
// special normalization for script tags without any armor
34
// our "armor" heurstic is a < sign any number of whitespaces after
35
// the first script tag
36
if ($config->get('HTML.Trusted')) {
37
$html = preg_replace_callback('#(<script[^>]*>)(\s*[^<].+?)(</script>)#si',
38
array($this, 'scriptCallback'), $html);
41
$html = $this->normalize($html, $config, $context);
43
$cursor = 0; // our location in the text
44
$inside_tag = false; // whether or not we're parsing the inside of a tag
45
$array = array(); // result array
47
// This is also treated to mean maintain *column* numbers too
48
$maintain_line_numbers = $config->get('Core.MaintainLineNumbers');
50
if ($maintain_line_numbers === null) {
51
// automatically determine line numbering by checking
52
// if error collection is on
53
$maintain_line_numbers = $config->get('Core.CollectErrors');
56
if ($maintain_line_numbers) {
59
$length = strlen($html);
61
$current_line = false;
65
$context->register('CurrentLine', $current_line);
66
$context->register('CurrentCol', $current_col);
68
// how often to manually recalculate. This will ALWAYS be right,
69
// but it's pretty wasteful. Set to 0 to turn off
70
$synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval');
73
if ($config->get('Core.CollectErrors')) {
74
$e =& $context->get('ErrorCollector');
77
// for testing synchronization
82
// $cursor is either at the start of a token, or inside of
83
// a tag (i.e. there was a < immediately before it), as indicated
86
if ($maintain_line_numbers) {
88
// $rcursor, however, is always at the start of a token.
89
$rcursor = $cursor - (int) $inside_tag;
91
// Column number is cheap, so we calculate it every round.
92
// We're interested at the *end* of the newline string, so
93
// we need to add strlen($nl) == 1 to $nl_pos before subtracting it
94
// from our "rcursor" position.
95
$nl_pos = strrpos($html, $nl, $rcursor - $length);
96
$current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1);
100
$synchronize_interval && // synchronization is on
101
$cursor > 0 && // cursor is further than zero
102
$loops % $synchronize_interval === 0 // time to synchronize!
104
$current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
109
$position_next_lt = strpos($html, '<', $cursor);
110
$position_next_gt = strpos($html, '>', $cursor);
112
// triggers on "<b>asdf</b>" but not "asdf <b></b>"
113
// special case to set up context
114
if ($position_next_lt === $cursor) {
119
if (!$inside_tag && $position_next_lt !== false) {
120
// We are not inside tag and there still is another tag to parse
122
HTMLPurifier_Token_Text(
125
$html, $cursor, $position_next_lt - $cursor
129
if ($maintain_line_numbers) {
130
$token->rawPosition($current_line, $current_col);
131
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor);
134
$cursor = $position_next_lt + 1;
137
} elseif (!$inside_tag) {
138
// We are not inside tag but there are no more tags
139
// If we're already at the end, break
140
if ($cursor === strlen($html)) break;
141
// Create Text of rest of string
143
HTMLPurifier_Token_Text(
150
if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
153
} elseif ($inside_tag && $position_next_gt !== false) {
154
// We are in tag and it is well formed
155
// Grab the internals of the tag
156
$strlen_segment = $position_next_gt - $cursor;
158
if ($strlen_segment < 1) {
159
// there's nothing to process!
160
$token = new HTMLPurifier_Token_Text('<');
165
$segment = substr($html, $cursor, $strlen_segment);
167
if ($segment === false) {
168
// somehow, we attempted to access beyond the end of
169
// the string, defense-in-depth, reported by Nate Abele
173
// Check if it's a comment
175
substr($segment, 0, 3) === '!--'
177
// re-determine segment length, looking for -->
178
$position_comment_end = strpos($html, '-->', $cursor);
179
if ($position_comment_end === false) {
180
// uh oh, we have a comment that extends to
181
// infinity. Can't be helped: set comment
182
// end position to end of string
183
if ($e) $e->send(E_WARNING, 'Lexer: Unclosed comment');
184
$position_comment_end = strlen($html);
189
$strlen_segment = $position_comment_end - $cursor;
190
$segment = substr($html, $cursor, $strlen_segment);
192
HTMLPurifier_Token_Comment(
194
$segment, 3, $strlen_segment - 3
197
if ($maintain_line_numbers) {
198
$token->rawPosition($current_line, $current_col);
199
$current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment);
202
$cursor = $end ? $position_comment_end : $position_comment_end + 3;
207
// Check if it's an end tag
208
$is_end_tag = (strpos($segment,'/') === 0);
210
$type = substr($segment, 1);
211
$token = new HTMLPurifier_Token_End($type);
212
if ($maintain_line_numbers) {
213
$token->rawPosition($current_line, $current_col);
214
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
218
$cursor = $position_next_gt + 1;
222
// Check leading character is alnum, if not, we may
223
// have accidently grabbed an emoticon. Translate into
224
// text and go our merry way
225
if (!ctype_alpha($segment[0])) {
226
// XML: $segment[0] !== '_' && $segment[0] !== ':'
227
if ($e) $e->send(E_NOTICE, 'Lexer: Unescaped lt');
228
$token = new HTMLPurifier_Token_Text('<');
229
if ($maintain_line_numbers) {
230
$token->rawPosition($current_line, $current_col);
231
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
238
// Check if it is explicitly self closing, if so, remove
239
// trailing slash. Remember, we could have a tag like <br>, so
240
// any later token processing scripts must convert improperly
241
// classified EmptyTags from StartTags.
242
$is_self_closing = (strrpos($segment,'/') === $strlen_segment-1);
243
if ($is_self_closing) {
245
$segment = substr($segment, 0, $strlen_segment);
248
// Check if there are any attributes
249
$position_first_space = strcspn($segment, $this->_whitespace);
251
if ($position_first_space >= $strlen_segment) {
252
if ($is_self_closing) {
253
$token = new HTMLPurifier_Token_Empty($segment);
255
$token = new HTMLPurifier_Token_Start($segment);
257
if ($maintain_line_numbers) {
258
$token->rawPosition($current_line, $current_col);
259
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
263
$cursor = $position_next_gt + 1;
267
// Grab out all the data
268
$type = substr($segment, 0, $position_first_space);
272
$segment, $position_first_space
275
if ($attribute_string) {
276
$attr = $this->parseAttributeString(
284
if ($is_self_closing) {
285
$token = new HTMLPurifier_Token_Empty($type, $attr);
287
$token = new HTMLPurifier_Token_Start($type, $attr);
289
if ($maintain_line_numbers) {
290
$token->rawPosition($current_line, $current_col);
291
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
294
$cursor = $position_next_gt + 1;
298
// inside tag, but there's no ending > sign
299
if ($e) $e->send(E_WARNING, 'Lexer: Missing gt');
301
HTMLPurifier_Token_Text(
304
substr($html, $cursor)
307
if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col);
308
// no cursor scroll? Hmm...
315
$context->destroy('CurrentLine');
316
$context->destroy('CurrentCol');
321
* PHP 5.0.x compatible substr_count that implements offset and length
323
protected function substrCount($haystack, $needle, $offset, $length) {
325
if ($oldVersion === null) {
326
$oldVersion = version_compare(PHP_VERSION, '5.1', '<');
329
$haystack = substr($haystack, $offset, $length);
330
return substr_count($haystack, $needle);
332
return substr_count($haystack, $needle, $offset, $length);
337
* Takes the inside of an HTML tag and makes an assoc array of attributes.
339
* @param $string Inside of tag excluding name.
340
* @returns Assoc array of attributes.
342
public function parseAttributeString($string, $config, $context) {
343
$string = (string) $string; // quick typecast
345
if ($string == '') return array(); // no attributes
348
if ($config->get('Core.CollectErrors')) {
349
$e =& $context->get('ErrorCollector');
352
// let's see if we can abort as quickly as possible
353
// one equal sign, no spaces => one attribute
354
$num_equal = substr_count($string, '=');
355
$has_space = strpos($string, ' ');
356
if ($num_equal === 0 && !$has_space) {
358
return array($string => $string);
359
} elseif ($num_equal === 1 && !$has_space) {
360
// only one attribute
361
list($key, $quoted_value) = explode('=', $string);
362
$quoted_value = trim($quoted_value);
364
if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
367
if (!$quoted_value) return array($key => '');
368
$first_char = @$quoted_value[0];
369
$last_char = @$quoted_value[strlen($quoted_value)-1];
371
$same_quote = ($first_char == $last_char);
372
$open_quote = ($first_char == '"' || $first_char == "'");
374
if ( $same_quote && $open_quote) {
376
$value = substr($quoted_value, 1, strlen($quoted_value) - 2);
380
if ($e) $e->send(E_ERROR, 'Lexer: Missing end quote');
381
$value = substr($quoted_value, 1);
383
$value = $quoted_value;
386
if ($value === false) $value = '';
387
return array($key => $this->parseData($value));
390
// setup loop environment
391
$array = array(); // return assoc array of attributes
392
$cursor = 0; // current position in string (moves forward)
393
$size = strlen($string); // size of the string (stays the same)
395
// if we have unquoted attributes, the parser expects a terminating
396
// space, so let's guarantee that there's always a terminating space.
401
if ($cursor >= $size) {
405
$cursor += ($value = strspn($string, $this->_whitespace, $cursor));
408
$key_begin = $cursor; //we're currently at the start of the key
410
// scroll past all characters that are the key (not whitespace or =)
411
$cursor += strcspn($string, $this->_whitespace . '=', $cursor);
413
$key_end = $cursor; // now at the end of the key
415
$key = substr($string, $key_begin, $key_end - $key_begin);
418
if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
419
$cursor += strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop
420
continue; // empty key
423
// scroll past all whitespace
424
$cursor += strspn($string, $this->_whitespace, $cursor);
426
if ($cursor >= $size) {
431
// if the next character is an equal sign, we've got a regular
432
// pair, otherwise, it's a bool attribute
433
$first_char = @$string[$cursor];
435
if ($first_char == '=') {
439
$cursor += strspn($string, $this->_whitespace, $cursor);
441
if ($cursor === false) {
446
// we might be in front of a quote right now
448
$char = @$string[$cursor];
450
if ($char == '"' || $char == "'") {
451
// it's quoted, end bound is $char
453
$value_begin = $cursor;
454
$cursor = strpos($string, $char, $cursor);
455
$value_end = $cursor;
457
// it's not quoted, end bound is whitespace
458
$value_begin = $cursor;
459
$cursor += strcspn($string, $this->_whitespace, $cursor);
460
$value_end = $cursor;
463
// we reached a premature end
464
if ($cursor === false) {
466
$value_end = $cursor;
469
$value = substr($string, $value_begin, $value_end - $value_begin);
470
if ($value === false) $value = '';
471
$array[$key] = $this->parseData($value);
479
// purely theoretical
480
if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key');
490
// vim: et sw=4 sts=4
4
* Our in-house implementation of a parser.
6
* A pure PHP parser, DirectLex has absolutely no dependencies, making
7
* it a reasonably good default for PHP4. Written with efficiency in mind,
8
* it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it
9
* pales in comparison to HTMLPurifier_Lexer_DOMLex.
11
* @todo Reread XML spec and document differences.
13
class HTMLPurifier_Lexer_DirectLex extends HTMLPurifier_Lexer
18
public $tracksLineNumbers = true;
21
* Whitespace characters for str(c)spn.
24
protected $_whitespace = "\x20\x09\x0D\x0A";
27
* Callback function for script CDATA fudge
28
* @param array $matches, in form of array(opening tag, contents, closing tag)
31
protected function scriptCallback($matches)
33
return $matches[1] . htmlspecialchars($matches[2], ENT_COMPAT, 'UTF-8') . $matches[3];
38
* @param HTMLPurifier_Config $config
39
* @param HTMLPurifier_Context $context
40
* @return array|HTMLPurifier_Token[]
42
public function tokenizeHTML($html, $config, $context)
44
// special normalization for script tags without any armor
45
// our "armor" heurstic is a < sign any number of whitespaces after
46
// the first script tag
47
if ($config->get('HTML.Trusted')) {
48
$html = preg_replace_callback(
49
'#(<script[^>]*>)(\s*[^<].+?)(</script>)#si',
50
array($this, 'scriptCallback'),
55
$html = $this->normalize($html, $config, $context);
57
$cursor = 0; // our location in the text
58
$inside_tag = false; // whether or not we're parsing the inside of a tag
59
$array = array(); // result array
61
// This is also treated to mean maintain *column* numbers too
62
$maintain_line_numbers = $config->get('Core.MaintainLineNumbers');
64
if ($maintain_line_numbers === null) {
65
// automatically determine line numbering by checking
66
// if error collection is on
67
$maintain_line_numbers = $config->get('Core.CollectErrors');
70
if ($maintain_line_numbers) {
73
$length = strlen($html);
75
$current_line = false;
79
$context->register('CurrentLine', $current_line);
80
$context->register('CurrentCol', $current_col);
82
// how often to manually recalculate. This will ALWAYS be right,
83
// but it's pretty wasteful. Set to 0 to turn off
84
$synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval');
87
if ($config->get('Core.CollectErrors')) {
88
$e =& $context->get('ErrorCollector');
91
// for testing synchronization
95
// $cursor is either at the start of a token, or inside of
96
// a tag (i.e. there was a < immediately before it), as indicated
99
if ($maintain_line_numbers) {
100
// $rcursor, however, is always at the start of a token.
101
$rcursor = $cursor - (int)$inside_tag;
103
// Column number is cheap, so we calculate it every round.
104
// We're interested at the *end* of the newline string, so
105
// we need to add strlen($nl) == 1 to $nl_pos before subtracting it
106
// from our "rcursor" position.
107
$nl_pos = strrpos($html, $nl, $rcursor - $length);
108
$current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1);
111
if ($synchronize_interval && // synchronization is on
112
$cursor > 0 && // cursor is further than zero
113
$loops % $synchronize_interval === 0) { // time to synchronize!
114
$current_line = 1 + $this->substrCount($html, $nl, 0, $cursor);
118
$position_next_lt = strpos($html, '<', $cursor);
119
$position_next_gt = strpos($html, '>', $cursor);
121
// triggers on "<b>asdf</b>" but not "asdf <b></b>"
122
// special case to set up context
123
if ($position_next_lt === $cursor) {
128
if (!$inside_tag && $position_next_lt !== false) {
129
// We are not inside tag and there still is another tag to parse
131
HTMLPurifier_Token_Text(
136
$position_next_lt - $cursor
140
if ($maintain_line_numbers) {
141
$token->rawPosition($current_line, $current_col);
142
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor);
145
$cursor = $position_next_lt + 1;
148
} elseif (!$inside_tag) {
149
// We are not inside tag but there are no more tags
150
// If we're already at the end, break
151
if ($cursor === strlen($html)) {
154
// Create Text of rest of string
156
HTMLPurifier_Token_Text(
164
if ($maintain_line_numbers) {
165
$token->rawPosition($current_line, $current_col);
169
} elseif ($inside_tag && $position_next_gt !== false) {
170
// We are in tag and it is well formed
171
// Grab the internals of the tag
172
$strlen_segment = $position_next_gt - $cursor;
174
if ($strlen_segment < 1) {
175
// there's nothing to process!
176
$token = new HTMLPurifier_Token_Text('<');
181
$segment = substr($html, $cursor, $strlen_segment);
183
if ($segment === false) {
184
// somehow, we attempted to access beyond the end of
185
// the string, defense-in-depth, reported by Nate Abele
189
// Check if it's a comment
190
if (substr($segment, 0, 3) === '!--') {
191
// re-determine segment length, looking for -->
192
$position_comment_end = strpos($html, '-->', $cursor);
193
if ($position_comment_end === false) {
194
// uh oh, we have a comment that extends to
195
// infinity. Can't be helped: set comment
196
// end position to end of string
198
$e->send(E_WARNING, 'Lexer: Unclosed comment');
200
$position_comment_end = strlen($html);
205
$strlen_segment = $position_comment_end - $cursor;
206
$segment = substr($html, $cursor, $strlen_segment);
208
HTMLPurifier_Token_Comment(
215
if ($maintain_line_numbers) {
216
$token->rawPosition($current_line, $current_col);
217
$current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment);
220
$cursor = $end ? $position_comment_end : $position_comment_end + 3;
225
// Check if it's an end tag
226
$is_end_tag = (strpos($segment, '/') === 0);
228
$type = substr($segment, 1);
229
$token = new HTMLPurifier_Token_End($type);
230
if ($maintain_line_numbers) {
231
$token->rawPosition($current_line, $current_col);
232
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
236
$cursor = $position_next_gt + 1;
240
// Check leading character is alnum, if not, we may
241
// have accidently grabbed an emoticon. Translate into
242
// text and go our merry way
243
if (!ctype_alpha($segment[0])) {
244
// XML: $segment[0] !== '_' && $segment[0] !== ':'
246
$e->send(E_NOTICE, 'Lexer: Unescaped lt');
248
$token = new HTMLPurifier_Token_Text('<');
249
if ($maintain_line_numbers) {
250
$token->rawPosition($current_line, $current_col);
251
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
258
// Check if it is explicitly self closing, if so, remove
259
// trailing slash. Remember, we could have a tag like <br>, so
260
// any later token processing scripts must convert improperly
261
// classified EmptyTags from StartTags.
262
$is_self_closing = (strrpos($segment, '/') === $strlen_segment - 1);
263
if ($is_self_closing) {
265
$segment = substr($segment, 0, $strlen_segment);
268
// Check if there are any attributes
269
$position_first_space = strcspn($segment, $this->_whitespace);
271
if ($position_first_space >= $strlen_segment) {
272
if ($is_self_closing) {
273
$token = new HTMLPurifier_Token_Empty($segment);
275
$token = new HTMLPurifier_Token_Start($segment);
277
if ($maintain_line_numbers) {
278
$token->rawPosition($current_line, $current_col);
279
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
283
$cursor = $position_next_gt + 1;
287
// Grab out all the data
288
$type = substr($segment, 0, $position_first_space);
293
$position_first_space
296
if ($attribute_string) {
297
$attr = $this->parseAttributeString(
306
if ($is_self_closing) {
307
$token = new HTMLPurifier_Token_Empty($type, $attr);
309
$token = new HTMLPurifier_Token_Start($type, $attr);
311
if ($maintain_line_numbers) {
312
$token->rawPosition($current_line, $current_col);
313
$current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor);
316
$cursor = $position_next_gt + 1;
320
// inside tag, but there's no ending > sign
322
$e->send(E_WARNING, 'Lexer: Missing gt');
325
HTMLPurifier_Token_Text(
328
substr($html, $cursor)
331
if ($maintain_line_numbers) {
332
$token->rawPosition($current_line, $current_col);
334
// no cursor scroll? Hmm...
341
$context->destroy('CurrentLine');
342
$context->destroy('CurrentCol');
347
* PHP 5.0.x compatible substr_count that implements offset and length
348
* @param string $haystack
349
* @param string $needle
354
protected function substrCount($haystack, $needle, $offset, $length)
357
if ($oldVersion === null) {
358
$oldVersion = version_compare(PHP_VERSION, '5.1', '<');
361
$haystack = substr($haystack, $offset, $length);
362
return substr_count($haystack, $needle);
364
return substr_count($haystack, $needle, $offset, $length);
369
* Takes the inside of an HTML tag and makes an assoc array of attributes.
371
* @param string $string Inside of tag excluding name.
372
* @param HTMLPurifier_Config $config
373
* @param HTMLPurifier_Context $context
374
* @return array Assoc array of attributes.
376
public function parseAttributeString($string, $config, $context)
378
$string = (string)$string; // quick typecast
385
if ($config->get('Core.CollectErrors')) {
386
$e =& $context->get('ErrorCollector');
389
// let's see if we can abort as quickly as possible
390
// one equal sign, no spaces => one attribute
391
$num_equal = substr_count($string, '=');
392
$has_space = strpos($string, ' ');
393
if ($num_equal === 0 && !$has_space) {
395
return array($string => $string);
396
} elseif ($num_equal === 1 && !$has_space) {
397
// only one attribute
398
list($key, $quoted_value) = explode('=', $string);
399
$quoted_value = trim($quoted_value);
402
$e->send(E_ERROR, 'Lexer: Missing attribute key');
406
if (!$quoted_value) {
407
return array($key => '');
409
$first_char = @$quoted_value[0];
410
$last_char = @$quoted_value[strlen($quoted_value) - 1];
412
$same_quote = ($first_char == $last_char);
413
$open_quote = ($first_char == '"' || $first_char == "'");
415
if ($same_quote && $open_quote) {
417
$value = substr($quoted_value, 1, strlen($quoted_value) - 2);
422
$e->send(E_ERROR, 'Lexer: Missing end quote');
424
$value = substr($quoted_value, 1);
426
$value = $quoted_value;
429
if ($value === false) {
432
return array($key => $this->parseData($value));
435
// setup loop environment
436
$array = array(); // return assoc array of attributes
437
$cursor = 0; // current position in string (moves forward)
438
$size = strlen($string); // size of the string (stays the same)
440
// if we have unquoted attributes, the parser expects a terminating
441
// space, so let's guarantee that there's always a terminating space.
445
while ($cursor < $size) {
446
if ($old_cursor >= $cursor) {
447
throw new Exception("Infinite loop detected");
449
$old_cursor = $cursor;
451
$cursor += ($value = strspn($string, $this->_whitespace, $cursor));
454
$key_begin = $cursor; //we're currently at the start of the key
456
// scroll past all characters that are the key (not whitespace or =)
457
$cursor += strcspn($string, $this->_whitespace . '=', $cursor);
459
$key_end = $cursor; // now at the end of the key
461
$key = substr($string, $key_begin, $key_end - $key_begin);
465
$e->send(E_ERROR, 'Lexer: Missing attribute key');
467
$cursor += 1 + strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop
468
continue; // empty key
471
// scroll past all whitespace
472
$cursor += strspn($string, $this->_whitespace, $cursor);
474
if ($cursor >= $size) {
479
// if the next character is an equal sign, we've got a regular
480
// pair, otherwise, it's a bool attribute
481
$first_char = @$string[$cursor];
483
if ($first_char == '=') {
487
$cursor += strspn($string, $this->_whitespace, $cursor);
489
if ($cursor === false) {
494
// we might be in front of a quote right now
496
$char = @$string[$cursor];
498
if ($char == '"' || $char == "'") {
499
// it's quoted, end bound is $char
501
$value_begin = $cursor;
502
$cursor = strpos($string, $char, $cursor);
503
$value_end = $cursor;
505
// it's not quoted, end bound is whitespace
506
$value_begin = $cursor;
507
$cursor += strcspn($string, $this->_whitespace, $cursor);
508
$value_end = $cursor;
511
// we reached a premature end
512
if ($cursor === false) {
514
$value_end = $cursor;
517
$value = substr($string, $value_begin, $value_end - $value_begin);
518
if ($value === false) {
521
$array[$key] = $this->parseData($value);
528
// purely theoretical
530
$e->send(E_ERROR, 'Lexer: Missing attribute key');
539
// vim: et sw=4 sts=4