5 Copyright 2009 Geoffrey Sneddon <http://gsnedders.com/>
7 Permission is hereby granted, free of charge, to any person obtaining a
8 copy of this software and associated documentation files (the
9 "Software"), to deal in the Software without restriction, including
10 without limitation the rights to use, copy, modify, merge, publish,
11 distribute, sublicense, and/or sell copies of the Software, and to
12 permit persons to whom the Software is furnished to do so, subject to
13 the following conditions:
15 The above copyright notice and this permission notice shall be included
16 in all copies or substantial portions of the Software.
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19 OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
21 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
22 CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
23 TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
24 SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
29 // /* */ indicates verbatim text from the HTML 5 specification
30 // // indicates regular comments
32 class HTML5_InputStream {
34 * The string data we're parsing.
39 * The current integer byte position we are in $data
44 * Length of $data; when $char === $data, we are at the end-of-file.
51 public $errors = array();
54 * @param $data Data to parse
56 public function __construct($data) {
58 /* Given an encoding, the bytes in the input stream must be
59 converted to Unicode characters for the tokeniser, as
60 described by the rules for that encoding, except that the
61 leading U+FEFF BYTE ORDER MARK character, if any, must not
62 be stripped by the encoding layer (it is stripped by the rule below).
64 Bytes or sequences of bytes in the original byte stream that
65 could not be converted to Unicode characters must be converted
66 to U+FFFD REPLACEMENT CHARACTER code points. */
68 // XXX currently assuming input data is UTF-8; once we
69 // build encoding detection this will no longer be the case
71 // We previously had an mbstring implementation here, but that
72 // implementation is heavily non-conforming, so it's been
74 if (extension_loaded('iconv')) {
76 $data = @iconv('UTF-8', 'UTF-8//IGNORE', $data);
78 // we can make a conforming native implementation
79 throw new Exception('Not implemented, please install mbstring or iconv');
82 /* One leading U+FEFF BYTE ORDER MARK character must be
83 ignored if any are present. */
84 if (substr($data, 0, 3) === "\xEF\xBB\xBF") {
85 $data = substr($data, 3);
88 /* All U+0000 NULL characters in the input must be replaced
89 by U+FFFD REPLACEMENT CHARACTERs. Any occurrences of such
90 characters is a parse error. */
91 for ($i = 0, $count = substr_count($data, "\0"); $i < $count; $i++) {
92 $this->errors[] = array(
93 'type' => HTML5_Tokenizer::PARSEERROR,
94 'data' => 'null-character'
97 /* U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED
98 (LF) characters are treated specially. Any CR characters
99 that are followed by LF characters must be removed, and any
100 CR characters not followed by LF characters must be converted
101 to LF characters. Thus, newlines in HTML DOMs are represented
102 by LF characters, and there are never any CR characters in the
103 input to the tokenization stage. */
118 /* Any occurrences of any characters in the ranges U+0001 to
119 U+0008, U+000B, U+000E to U+001F, U+007F to U+009F,
120 U+D800 to U+DFFF , U+FDD0 to U+FDEF, and
121 characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, U+2FFFE, U+2FFFF,
122 U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE,
123 U+6FFFF, U+7FFFE, U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF,
124 U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, U+DFFFE,
125 U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and
126 U+10FFFF are parse errors. (These are all control characters
127 or permanently undefined Unicode characters.) */
128 // Check PCRE is loaded.
129 if (extension_loaded('pcre')) {
130 $count = preg_match_all(
132 [\x01-\x08\x0B\x0E-\x1F\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F
134 \xC2[\x80-\x9F] # U+0080 to U+009F
136 \xED(?:\xA0[\x80-\xFF]|[\xA1-\xBE][\x00-\xFF]|\xBF[\x00-\xBF]) # U+D800 to U+DFFFF
138 \xEF\xB7[\x90-\xAF] # U+FDD0 to U+FDEF
140 \xEF\xBF[\xBE\xBF] # U+FFFE and U+FFFF
142 [\xF0-\xF4][\x8F-\xBF]\xBF[\xBE\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16})
147 for ($i = 0; $i < $count; $i++) {
148 $this->errors[] = array(
149 'type' => HTML5_Tokenizer::PARSEERROR,
150 'data' => 'invalid-codepoint'
154 // XXX: Need non-PCRE impl, probably using substr_count
159 $this->EOF = strlen($data);
163 * Returns the current line that the tokenizer is at.
165 public function getCurrentLine() {
166 // Check the string isn't empty
168 // Add one to $this->char because we want the number for the next
169 // byte to be processed.
170 return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1;
172 // If the string is empty, we are on the first line (sorta).
178 * Returns the current column of the current line that the tokenizer is at.
180 public function getColumnOffset() {
181 // strrpos is weird, and the offset needs to be negative for what we
182 // want (i.e., the last \n before $this->char). This needs to not have
183 // one (to make it point to the next character, the one we want the
184 // position of) added to it because strrpos's behaviour includes the
185 // final offset byte.
186 $lastLine = strrpos($this->data, "\n", $this->char - 1 - strlen($this->data));
188 // However, for here we want the length up until the next byte to be
189 // processed, so add one to the current byte ($this->char).
190 if($lastLine !== false) {
191 $findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine);
193 $findLengthOf = substr($this->data, 0, $this->char);
196 // Get the length for the string we need.
197 if(extension_loaded('iconv')) {
198 return iconv_strlen($findLengthOf, 'utf-8');
199 } elseif(extension_loaded('mbstring')) {
200 return mb_strlen($findLengthOf, 'utf-8');
201 } elseif(extension_loaded('xml')) {
202 return strlen(utf8_decode($findLengthOf));
204 $count = count_chars($findLengthOf);
205 // 0x80 = 0x7F - 0 + 1 (one added to get inclusive range)
206 // 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range)
207 return array_sum(array_slice($count, 0, 0x80)) +
208 array_sum(array_slice($count, 0xC2, 0x33));
213 * Retrieve the currently consume character.
214 * @note This performs bounds checking
216 public function char() {
217 return ($this->char++ < $this->EOF)
218 ? $this->data[$this->char - 1]
223 * Get all characters until EOF.
224 * @note This performs bounds checking
226 public function remainingChars() {
227 if($this->char < $this->EOF) {
228 $data = substr($this->data, $this->char);
229 $this->char = $this->EOF;
237 * Matches as far as possible until we reach a certain set of bytes
238 * and returns the matched substring.
239 * @param $bytes Bytes to match.
241 public function charsUntil($bytes, $max = null) {
242 if ($this->char < $this->EOF) {
243 if ($max === 0 || $max) {
244 $len = strcspn($this->data, $bytes, $this->char, $max);
246 $len = strcspn($this->data, $bytes, $this->char);
248 $string = (string) substr($this->data, $this->char, $len);
257 * Matches as far as possible with a certain set of bytes
258 * and returns the matched substring.
259 * @param $bytes Bytes to match.
261 public function charsWhile($bytes, $max = null) {
262 if ($this->char < $this->EOF) {
263 if ($max === 0 || $max) {
264 $len = strspn($this->data, $bytes, $this->char, $max);
266 $len = strspn($this->data, $bytes, $this->char);
268 $string = (string) substr($this->data, $this->char, $len);
277 * Unconsume one character.
279 public function unget() {
280 if ($this->char <= $this->EOF) {