summaryrefslogtreecommitdiff
path: root/src/HTML5/Parser
diff options
context:
space:
mode:
authorTitouan Galopin <[email protected]>2018-11-03 01:03:34 +0100
committerTitouan Galopin <[email protected]>2018-11-03 01:36:52 +0100
commit321ed9626c091f1f4dcef8223d88ee88a400a241 (patch)
tree32a565fac06feba41230228a806dc22d6e8f17c3 /src/HTML5/Parser
parent7453ab08dc4c8f65d5db52fd2c6b8943d59bf95b (diff)
Improve performance by relying on a native string instead of InputStream
Diffstat (limited to 'src/HTML5/Parser')
-rw-r--r--src/HTML5/Parser/FileInputStream.php3
-rw-r--r--src/HTML5/Parser/InputStream.php4
-rw-r--r--src/HTML5/Parser/Scanner.php237
-rw-r--r--src/HTML5/Parser/StringInputStream.php9
4 files changed, 216 insertions, 37 deletions
diff --git a/src/HTML5/Parser/FileInputStream.php b/src/HTML5/Parser/FileInputStream.php
index fbf006d..76bd17b 100644
--- a/src/HTML5/Parser/FileInputStream.php
+++ b/src/HTML5/Parser/FileInputStream.php
@@ -11,11 +11,10 @@ namespace Masterminds\HTML5\Parser;
* really like to rewrite this class to efficiently handle lower level
* stream reads (and thus efficiently handle large documents).
*
- * @todo A buffered input stream would be useful.
+ * @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
*/
class FileInputStream extends StringInputStream implements InputStream
{
-
/**
* Load a file input stream.
*
diff --git a/src/HTML5/Parser/InputStream.php b/src/HTML5/Parser/InputStream.php
index 0bdc803..e4a106a 100644
--- a/src/HTML5/Parser/InputStream.php
+++ b/src/HTML5/Parser/InputStream.php
@@ -1,4 +1,5 @@
<?php
+
namespace Masterminds\HTML5\Parser;
/**
@@ -9,10 +10,11 @@ namespace Masterminds\HTML5\Parser;
*
* Currently provided InputStream implementations include
* FileInputStream and StringInputStream.
+ *
+ * @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
*/
interface InputStream extends \Iterator
{
-
/**
* Returns the current line that is being consumed.
*
diff --git a/src/HTML5/Parser/Scanner.php b/src/HTML5/Parser/Scanner.php
index f605c69..dc685bb 100644
--- a/src/HTML5/Parser/Scanner.php
+++ b/src/HTML5/Parser/Scanner.php
@@ -1,34 +1,64 @@
<?php
namespace Masterminds\HTML5\Parser;
+use Masterminds\HTML5\Exception;
+
/**
- * The scanner.
- *
- * This scans over an input stream.
+ * The scanner scans over a given data input to react appropriately to characters.
*/
class Scanner
{
-
const CHARS_HEX = 'abcdefABCDEF01234567890';
-
const CHARS_ALNUM = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890';
-
const CHARS_ALPHA = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
- protected $is;
+ /**
+ * The string data we're parsing.
+ */
+ private $data;
- // Flipping this to true will give minisculely more debugging info.
- public $debug = false;
+ /**
+ * The current integer byte position we are in $data
+ */
+ private $char;
+
+ /**
+ * Length of $data; when $char === $data, we are at the end-of-file.
+ */
+ private $EOF;
+
+ /**
+ * Parse errors.
+ */
+ public $errors = array();
/**
* Create a new Scanner.
*
- * @param \Masterminds\HTML5\Parser\InputStream $input
- * An InputStream to be scanned.
+ * @param string $data Data to parse
+ * @param string $encoding The encoding to use for the data.
+ *
+ * @throws Exception If the given data cannot be encoded to UTF-8.
*/
- public function __construct($input)
+ public function __construct($data, $encoding = 'UTF-8')
{
- $this->is = $input;
+ if ($data instanceof InputStream) {
+ @trigger_error('InputStream objects are deprecated since version 2.4 and will be removed in 3.0. Use strings instead.', E_USER_DEPRECATED);
+ $data = (string) $data;
+ }
+
+ $data = UTF8Utils::convertToUTF8($data, $encoding);
+
+ // There is good reason to question whether it makes sense to
+ // do this here, since most of these checks are done during
+ // parsing, and since this check doesn't actually *do* anything.
+ $this->errors = UTF8Utils::checkForIllegalCodepoints($data);
+
+ $data = $this->replaceLinefeeds($data);
+
+ $this->data = $data;
+ $this->char = 0;
+ $this->EOF = strlen($data);
}
/**
@@ -38,7 +68,7 @@ class Scanner
*/
public function position()
{
- return $this->is->key();
+ return $this->char;
}
/**
@@ -48,7 +78,11 @@ class Scanner
*/
public function peek()
{
- return $this->is->peek();
+ if (($this->char + 1) <= $this->EOF) {
+ return $this->data[$this->char + 1];
+ }
+
+ return false;
}
/**
@@ -60,11 +94,10 @@ class Scanner
*/
public function next()
{
- $this->is->next();
- if ($this->is->valid()) {
- if ($this->debug)
- fprintf(STDOUT, "> %s\n", $this->is->current());
- return $this->is->current();
+ $this->char++;
+
+ if ($this->char < $this->EOF) {
+ return $this->data[$this->char];
}
return false;
@@ -79,8 +112,8 @@ class Scanner
*/
public function current()
{
- if ($this->is->valid()) {
- return $this->is->current();
+ if ($this->char < $this->EOF) {
+ return $this->data[$this->char];
}
return false;
@@ -88,6 +121,8 @@ class Scanner
/**
* Silently consume N chars.
+ *
+ * @param int $count
*/
public function consume($count = 1)
{
@@ -105,7 +140,9 @@ class Scanner
*/
public function unconsume($howMany = 1)
{
- $this->is->unconsume($howMany);
+ if (($this->char - $howMany) >= 0) {
+ $this->char = $this->char - $howMany;
+ }
}
/**
@@ -118,7 +155,7 @@ class Scanner
*/
public function getHex()
{
- return $this->is->charsWhile(static::CHARS_HEX);
+ return $this->doCharsWhile(static::CHARS_HEX);
}
/**
@@ -131,7 +168,7 @@ class Scanner
*/
public function getAsciiAlpha()
{
- return $this->is->charsWhile(static::CHARS_ALPHA);
+ return $this->doCharsWhile(static::CHARS_ALPHA);
}
/**
@@ -144,7 +181,7 @@ class Scanner
*/
public function getAsciiAlphaNum()
{
- return $this->is->charsWhile(static::CHARS_ALNUM);
+ return $this->doCharsWhile(static::CHARS_ALNUM);
}
/**
@@ -157,7 +194,7 @@ class Scanner
*/
public function getNumeric()
{
- return $this->is->charsWhile('0123456789');
+ return $this->doCharsWhile('0123456789');
}
/**
@@ -167,7 +204,7 @@ class Scanner
*/
public function whitespace()
{
- return $this->is->charsWhile("\n\t\f ");
+ return $this->doCharsWhile("\n\t\f ");
}
/**
@@ -177,23 +214,37 @@ class Scanner
*/
public function currentLine()
{
- return $this->is->currentLine();
+ if (empty($this->EOF) || $this->char == 0) {
+ return 1;
+ }
+
+ // Add one to $this->char because we want the number for the next
+ // byte to be processed.
+ return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1;
}
/**
* Read chars until something in the mask is encountered.
+ *
+ * @param string $mask
+ *
+ * @return mixed
*/
public function charsUntil($mask)
{
- return $this->is->charsUntil($mask);
+ return $this->doCharsUntil($mask);
}
/**
* Read chars as long as the mask matches.
+ *
+ * @param string $mask
+ *
+ * @return int
*/
public function charsWhile($mask)
{
- return $this->is->charsWhile($mask);
+ return $this->doCharsWhile($mask);
}
/**
@@ -205,7 +256,29 @@ class Scanner
*/
public function columnOffset()
{
- return $this->is->columnOffset();
+ // Short circuit for the first char.
+ if ($this->char == 0) {
+ return 0;
+ }
+
+ // strrpos is weird, and the offset needs to be negative for what we
+ // want (i.e., the last \n before $this->char). This needs to not have
+ // one (to make it point to the next character, the one we want the
+ // position of) added to it because strrpos's behaviour includes the
+ // final offset byte.
+ $backwardFrom = $this->char - 1 - strlen($this->data);
+ $lastLine = strrpos($this->data, "\n", $backwardFrom);
+
+ // However, for here we want the length up until the next byte to be
+ // processed, so add one to the current byte ($this->char).
+ if ($lastLine !== false) {
+ $findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine);
+ } else {
+ // After a newline.
+ $findLengthOf = substr($this->data, 0, $this->char);
+ }
+
+ return UTF8Utils::countChars($findLengthOf);
}
/**
@@ -217,6 +290,104 @@ class Scanner
*/
public function remainingChars()
{
- return $this->is->remainingChars();
+ if ($this->char < $this->EOF) {
+ $data = substr($this->data, $this->char);
+ $this->char = $this->EOF;
+
+ return $data;
+ }
+
+ return ''; // false;
+ }
+
+ /**
+ * Replace linefeed characters according to the spec.
+ *
+ * @param $data
+ *
+ * @return string
+ */
+ private function replaceLinefeeds($data)
+ {
+ /*
+ * U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED (LF) characters are treated specially.
+ * Any CR characters that are followed by LF characters must be removed, and any CR characters not
+ * followed by LF characters must be converted to LF characters. Thus, newlines in HTML DOMs are
+ * represented by LF characters, and there are never any CR characters in the input to the tokenization
+ * stage.
+ */
+ $crlfTable = array(
+ "\0" => "\xEF\xBF\xBD",
+ "\r\n" => "\n",
+ "\r" => "\n"
+ );
+
+ return strtr($data, $crlfTable);
+ }
+
+ /**
+ * Read to a particular match (or until $max bytes are consumed).
+ *
+ * This operates on byte sequences, not characters.
+ *
+ * Matches as far as possible until we reach a certain set of bytes
+ * and returns the matched substring.
+ *
+ * @param string $bytes
+ * Bytes to match.
+ * @param int $max
+ * Maximum number of bytes to scan.
+ * @return mixed Index or false if no match is found. You should use strong
+ * equality when checking the result, since index could be 0.
+ */
+ private function doCharsUntil($bytes, $max = null)
+ {
+ if ($this->char >= $this->EOF) {
+ return false;
+ }
+
+ if ($max === 0 || $max) {
+ $len = strcspn($this->data, $bytes, $this->char, $max);
+ } else {
+ $len = strcspn($this->data, $bytes, $this->char);
+ }
+
+ $string = (string) substr($this->data, $this->char, $len);
+ $this->char += $len;
+
+ return $string;
+ }
+
+ /**
+ * Returns the string so long as $bytes matches.
+ *
+ * Matches as far as possible with a certain set of bytes
+ * and returns the matched substring.
+ *
+ * @param string $bytes
+ * A mask of bytes to match. If ANY byte in this mask matches the
+ * current char, the pointer advances and the char is part of the
+ * substring.
+ * @param int $max
+ * The max number of chars to read.
+ *
+ * @return string
+ */
+ private function doCharsWhile($bytes, $max = null)
+ {
+ if ($this->char >= $this->EOF) {
+ return false;
+ }
+
+ if ($max === 0 || $max) {
+ $len = strspn($this->data, $bytes, $this->char, $max);
+ } else {
+ $len = strspn($this->data, $bytes, $this->char);
+ }
+
+ $string = (string) substr($this->data, $this->char, $len);
+ $this->char += $len;
+
+ return $string;
}
}
diff --git a/src/HTML5/Parser/StringInputStream.php b/src/HTML5/Parser/StringInputStream.php
index 0973941..0118468 100644
--- a/src/HTML5/Parser/StringInputStream.php
+++ b/src/HTML5/Parser/StringInputStream.php
@@ -39,9 +39,11 @@ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// - // indicates regular comments
+/**
+ * @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead.
+ */
class StringInputStream implements InputStream
{
-
/**
* The string data we're parsing.
*/
@@ -88,6 +90,11 @@ class StringInputStream implements InputStream
$this->EOF = strlen($data);
}
+ public function __toString()
+ {
+ return $this->data;
+ }
+
/**
* Replace linefeed characters according to the spec.
*/