3 require_once 'HTMLPurifier/Lexer.php';
6 * Our in-house implementation of a parser.
8 * A pure PHP parser, DirectLex has absolutely no dependencies, making
9 * it a reasonably good default for PHP4. Written with efficiency in mind,
10 * it can be four times faster than HTMLPurifier_Lexer_PEARSax3, although it
11 * pales in comparison to HTMLPurifier_Lexer_DOMLex. It will support UTF-8
12 * completely eventually.
14 * @todo Reread XML spec and document differences.
15 * @todo Add support for CDATA sections.
16 * @todo Determine correct behavior in outputting comment data. (preserve dashes?)
17 * @todo Optimize main function tokenizeHTML().
18 * @todo Less than sign (<) being prohibited (even as entity) in attr-values?
20 class HTMLPurifier_Lexer_DirectLex
extends HTMLPurifier_Lexer
24 * Most common entity to raw value conversion table for special entities.
27 var $_special_entity2str =
39 * Parses special entities into the proper characters.
41 * This string will translate escaped versions of the special characters
42 * into the correct ones.
45 * You should be able to treat the output of this function as
46 * completely parsed, but that's only because all other entities should
47 * have been handled previously in substituteNonSpecialEntities()
49 * @param $string String character data to be parsed.
50 * @returns Parsed character data.
52 function parseData($string) {
54 // subtracts amps that cannot possibly be escaped
55 $num_amp = substr_count($string, '&') - substr_count($string, '& ') -
56 ($string[strlen($string)-1] === '&' ?
1 : 0);
58 if (!$num_amp) return $string; // abort if no entities
59 $num_esc_amp = substr_count($string, '&');
60 $string = strtr($string, $this->_special_entity2str
);
62 // code duplication for sake of optimization, see above
63 $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') -
64 ($string[strlen($string)-1] === '&' ?
1 : 0);
66 if ($num_amp_2 <= $num_esc_amp) return $string;
68 // hmm... now we have some uncommon entities. Use the callback.
69 $string = $this->_encoder
->substituteSpecialEntities($string);
74 * Whitespace characters for str(c)spn.
77 var $_whitespace = "\x20\x09\x0D\x0A";
79 function tokenizeHTML($string, $config = null) {
81 if (!$config) $config = HTMLPurifier_Config
::createDefault();
83 // some quick checking (if empty, return empty)
84 $string = @ (string) $string;
85 if ($string == '') return array();
87 if ($config->get('Core', 'AcceptFullDocuments')) {
88 $string = $this->extractBody($string);
91 $cursor = 0; // our location in the text
92 $inside_tag = false; // whether or not we're parsing the inside of a tag
93 $array = array(); // result array
96 $string = $this->escapeCDATA($string);
98 // expand entities THAT AREN'T THE BIG FIVE
99 $string = $this->_encoder
->substituteNonSpecialEntities($string);
101 // clean it into wellformed UTF-8 string
102 $string = $this->_encoder
->cleanUTF8($string);
104 // infinite loop protection
105 // has to be pretty big, since html docs can be big
106 // we're allow two hundred thousand tags... more than enough?
111 // infinite loop protection
112 if (++
$loops > 200000) return array();
114 $position_next_lt = strpos($string, '<', $cursor);
115 $position_next_gt = strpos($string, '>', $cursor);
117 // triggers on "<b>asdf</b>" but not "asdf <b></b>"
118 if ($position_next_lt === $cursor) {
123 if (!$inside_tag && $position_next_lt !== false) {
124 // We are not inside tag and there still is another tag to parse
126 HTMLPurifier_Token_Text(
129 $string, $cursor, $position_next_lt - $cursor
133 $cursor = $position_next_lt +
1;
136 } elseif (!$inside_tag) {
137 // We are not inside tag but there are no more tags
138 // If we're already at the end, break
139 if ($cursor === strlen($string)) break;
140 // Create Text of rest of string
142 HTMLPurifier_Token_Text(
150 } elseif ($inside_tag && $position_next_gt !== false) {
151 // We are in tag and it is well formed
152 // Grab the internals of the tag
153 $strlen_segment = $position_next_gt - $cursor;
154 $segment = substr($string, $cursor, $strlen_segment);
156 // Check if it's a comment
158 substr($segment, 0, 3) == '!--' &&
159 substr($segment, $strlen_segment-2, 2) == '--'
162 HTMLPurifier_Token_Comment(
164 $segment, 3, $strlen_segment - 5
168 $cursor = $position_next_gt +
1;
172 // Check if it's an end tag
173 $is_end_tag = (strpos($segment,'/') === 0);
175 $type = substr($segment, 1);
176 $array[] = new HTMLPurifier_Token_End($type);
178 $cursor = $position_next_gt +
1;
182 // Check if it is explicitly self closing, if so, remove
183 // trailing slash. Remember, we could have a tag like <br>, so
184 // any later token processing scripts must convert improperly
185 // classified EmptyTags from StartTags.
186 $is_self_closing= (strpos($segment,'/') === $strlen_segment-1);
187 if ($is_self_closing) {
189 $segment = substr($segment, 0, $strlen_segment);
192 // Check if there are any attributes
193 $position_first_space = strcspn($segment, $this->_whitespace
);
195 if ($position_first_space >= $strlen_segment) {
196 if ($is_self_closing) {
197 $array[] = new HTMLPurifier_Token_Empty($segment);
199 $array[] = new HTMLPurifier_Token_Start($segment);
202 $cursor = $position_next_gt +
1;
206 // Grab out all the data
207 $type = substr($segment, 0, $position_first_space);
211 $segment, $position_first_space
214 if ($attribute_string) {
215 $attributes = $this->parseAttributeString(
219 $attributes = array();
222 if ($is_self_closing) {
223 $array[] = new HTMLPurifier_Token_Empty($type, $attributes);
225 $array[] = new HTMLPurifier_Token_Start($type, $attributes);
227 $cursor = $position_next_gt +
1;
232 HTMLPurifier_Token_Text(
235 substr($string, $cursor)
246 * Takes the inside of an HTML tag and makes an assoc array of attributes.
248 * @param $string Inside of tag excluding name.
249 * @returns Assoc array of attributes.
251 function parseAttributeString($string) {
252 $string = (string) $string; // quick typecast
254 if ($string == '') return array(); // no attributes
256 // let's see if we can abort as quickly as possible
257 // one equal sign, no spaces => one attribute
258 $num_equal = substr_count($string, '=');
259 $has_space = strpos($string, ' ');
260 if ($num_equal === 0 && !$has_space) {
262 return array($string => $string);
263 } elseif ($num_equal === 1 && !$has_space) {
264 // only one attribute
265 list($key, $quoted_value) = explode('=', $string);
266 $quoted_value = trim($quoted_value);
267 if (!$key) return array();
268 if (!$quoted_value) return array($key => '');
269 $first_char = @$quoted_value[0];
270 $last_char = @$quoted_value[strlen($quoted_value)-1];
272 $same_quote = ($first_char == $last_char);
273 $open_quote = ($first_char == '"' ||
$first_char == "'");
275 if ( $same_quote && $open_quote) {
277 $value = substr($quoted_value, 1, strlen($quoted_value) - 2);
281 $value = substr($quoted_value, 1);
283 $value = $quoted_value;
286 return array($key => $value);
289 // setup loop environment
290 $array = array(); // return assoc array of attributes
291 $cursor = 0; // current position in string (moves forward)
292 $size = strlen($string); // size of the string (stays the same)
294 // if we have unquoted attributes, the parser expects a terminating
295 // space, so let's guarantee that there's always a terminating space.
298 // infinite loop protection
303 // infinite loop protection
304 if (++
$loops > 1000) return array();
306 if ($cursor >= $size) {
310 $cursor +
= ($value = strspn($string, $this->_whitespace
, $cursor));
314 $key_begin = $cursor; //we're currently at the start of the key
316 // scroll past all characters that are the key (not whitespace or =)
317 $cursor +
= strcspn($string, $this->_whitespace
. '=', $cursor);
319 $key_end = $cursor; // now at the end of the key
321 $key = substr($string, $key_begin, $key_end - $key_begin);
323 if (!$key) continue; // empty key
325 // scroll past all whitespace
326 $cursor +
= strspn($string, $this->_whitespace
, $cursor);
328 if ($cursor >= $size) {
333 // if the next character is an equal sign, we've got a regular
334 // pair, otherwise, it's a bool attribute
335 $first_char = @$string[$cursor];
337 if ($first_char == '=') {
341 $cursor +
= strspn($string, $this->_whitespace
, $cursor);
343 // we might be in front of a quote right now
345 $char = @$string[$cursor];
347 if ($char == '"' ||
$char == "'") {
348 // it's quoted, end bound is $char
350 $value_begin = $cursor;
351 $cursor = strpos($string, $char, $cursor);
352 $value_end = $cursor;
354 // it's not quoted, end bound is whitespace
355 $value_begin = $cursor;
356 $cursor +
= strcspn($string, $this->_whitespace
, $cursor);
357 $value_end = $cursor;
360 $value = substr($string, $value_begin, $value_end - $value_begin);
361 $array[$key] = $this->parseData($value);