diff --git a/core/lib/Drupal/Component/XpathHelper/Lexer.php b/core/lib/Drupal/Component/XpathHelper/Lexer.php
new file mode 100644
index 0000000..8876aac
--- /dev/null
+++ b/core/lib/Drupal/Component/XpathHelper/Lexer.php
@@ -0,0 +1,240 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\XpathHelper\Lexer.
+ */
+
+namespace Drupal\Component\XpathHelper;
+
+/**
+ * Turns an XPath expression into a list of tokens.
+ */
+class Lexer {
+
+  /**
+   * The XPath expression being lexed.
+   *
+   * @var string
+   */
+  protected $expression;
+
+  /**
+   * The current position in the expression.
+   *
+   * @var int
+   */
+  protected $cursor;
+
+  /**
+   * The length of the XPath expression.
+   *
+   * @var int
+   */
+  protected $length;
+
+  /**
+   * Characters that represent word boundaries.
+   *
+   * @var array
+   */
+  protected static $wordBoundaries = array(
+    '[' => TRUE,
+    ']' => TRUE,
+    '=' => TRUE,
+    '(' => TRUE,
+    ')' => TRUE,
+    '.' => TRUE,
+    '<' => TRUE,
+    '>' => TRUE,
+    '*' => TRUE,
+    '+' => TRUE,
+    // Used in element names and functions. It's easier to just make a special
+    // case in the parser than to have the minus be a word boundary.
+    // '-' => TRUE,
+    '!' => TRUE,
+    '|' => TRUE,
+    ',' => TRUE,
+    ' ' => TRUE,
+    '"' => TRUE,
+    "'" => TRUE,
+    ':' => TRUE,
+    '::' => TRUE,
+    '/' => TRUE,
+    '//' => TRUE,
+    '@' => TRUE,
+  );
+
+  /**
+   * Lexes an XPath expression.
+   *
+   * @param string $expression
+   *   An XPath expression.
+   *
+   * @return array
+   *   A list of tokens from the XPath expression.
+   */
+  public function lex($expression) {
+    $this->expression = $expression;
+    $this->length = strlen($expression);
+    $this->cursor = 0;
+
+    $tokens = array();
+    while (TRUE) {
+      $token = $this->readToken();
+      if ($token === '') {
+        break;
+      }
+      $tokens[] = $token;
+    }
+
+    return $tokens;
+  }
+
+  /**
+   * Determines if a token is boundary for a word.
+   *
+   * @param string $token
+   *   The token.
+   *
+   * @return bool
+   *   Returns true if the token is a word boundary, and false if not.
+   */
+  public static function isWordBoundary($token) {
+    return isset(static::$wordBoundaries[$token]);
+  }
+
+  /**
+   * Reads the next token from the expression.
+   *
+   * @return string
+   *   The next token, or an empty string on completion.
+   */
+  protected function readToken() {
+    while ($this->cursor < $this->length) {
+      $char = $this->expression[$this->cursor];
+
+      if ($char === '/') {
+        return $this->readOneOrTwoSlashes($char);
+      }
+
+      if ($char === '"' || $char === "'") {
+        return $this->consumeQuotes($char);
+      }
+
+      if ($char === ':') {
+        return $this->readNamespaceOrAxis();
+      }
+
+      if ($char === '@') {
+        return $this->readAttribute();
+      }
+
+      if ($this->isWordBoundary($char)) {
+        $this->cursor++;
+        return $char;
+      }
+
+      return $this->readWord();
+    }
+
+    return '';
+  }
+
+  /**
+   * Reads the next word from the expression.
+   *
+   * A word is considered anything that isn't a word boundary.
+   *
+   * @return string
+   *   The next word.
+   */
+  protected function readWord() {
+    $word = '';
+
+    while ($this->cursor < $this->length) {
+      $char = $this->expression[$this->cursor];
+
+      // Found a boundary.
+      if ($this->isWordBoundary($char)) {
+        break;
+      }
+
+      $word .= $char;
+      $this->cursor++;
+    }
+
+    return $word;
+  }
+
+  /**
+   * Reads a quoted string from an XPath expression.
+   *
+   * @param string $start_quote
+   *   The character that started the quoted string.
+   *
+   * @return string
+   *   The quoted string.
+   */
+  protected function consumeQuotes($start_quote) {
+    $output = $start_quote;
+    do {
+      $next_char = $this->readNextChar();
+      $output .= $next_char;
+    } while ($next_char !== '' && $next_char !== $start_quote);
+
+    $this->cursor++;
+    return $output;
+  }
+
+  /**
+   * Reads a namespace token or an axis token.
+   *
+   * @return string
+   *   Either a namespace separator or an axis separator. One or two colons.
+   */
+  protected function readNamespaceOrAxis() {
+    if ($this->readNextChar() === ':') {
+      $this->cursor++;
+      return '::';
+    }
+    return ':';
+  }
+
+  /**
+   * Reads on or two slashes.
+   *
+   * @return string
+   *  Returns / or //.
+   */
+  protected function readOneOrTwoSlashes() {
+    if ($this->readNextChar() === '/') {
+      $this->cursor++;
+      return '//';
+    }
+    return '/';
+  }
+
+  /**
+   * Reads a shorthand attribute.
+   *
+   * @return string
+   *   An attribute string starting with @.
+   */
+  protected function readAttribute() {
+    $this->cursor++;
+    return '@' . $this->readWord();
+  }
+
+  /**
+   * Returns the next character advancing the cursor.
+   *
+   * @return string
+   *   The next character.
+   */
+  protected function readNextChar() {
+    $this->cursor++;
+    return isset($this->expression[$this->cursor]) ? $this->expression[$this->cursor] : '';
+  }
+
+}
diff --git a/core/lib/Drupal/Component/XpathHelper/Namespacer.php b/core/lib/Drupal/Component/XpathHelper/Namespacer.php
new file mode 100644
index 0000000..a5a2af7
--- /dev/null
+++ b/core/lib/Drupal/Component/XpathHelper/Namespacer.php
@@ -0,0 +1,351 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Component\XpathHelper\Namespacer.
+ */
+
+namespace Drupal\Component\XpathHelper;
+
+use Drupal\Component\XpathHelper\Lexer;
+
+/**
+ * XPath expressions namespacer.
+ *
+ * When a DOMDocument has a default namespace it's not possible to parse it
+ * using XPath unless the namespace is registered as something else. This class
+ * provides two methods for working around this limitation.
+ */
+class Namespacer {
+
+  /**
+   * The prefix to assign.
+   *
+   * @var string
+   */
+  protected $prefix;
+
+  /**
+   * The list of tokens from the XPath expression.
+   *
+   * @var []string
+   */
+  protected $tokens;
+
+  /**
+   * The current position in the token list.
+   *
+   * @var int
+   */
+  protected $cursor = 0;
+
+  /**
+   * A cache of rewritten expressions.
+   *
+   * @var array
+   */
+  protected static $cache = array();
+
+  /**
+   * Strings that have operational meaning and shouldn't be namespaced.
+   *
+   * @var array
+   */
+  protected static $operators = array(
+    'or' => TRUE,
+    'and' => TRUE,
+    'div' => TRUE,
+    'mod' => TRUE,
+  );
+
+  /**
+   * Tokens that come before elements.
+   *
+   * @var array
+   */
+  protected static $precedesElement = array(
+    '/' => TRUE,
+    '//' => TRUE,
+    '::' => TRUE,
+    '(' => TRUE,
+    ',' => TRUE,
+    '[' => TRUE,
+  );
+
+  /**
+   * Prefixes an XPath expression.
+   *
+   * Converts an expression from //div/a to //x:div/x:a.
+   *
+   * @param string $xpath
+   *   The XPath expression to prefix.
+   * @param string $prefix
+   *   (optional) The prefix to use. Defaults to "x".
+   *
+   * @return string
+   *   The prefixed XPath expression.
+   */
+  public static function prefix($xpath, $prefix = 'x') {
+    if (!isset(static::$cache[$prefix][$xpath])) {
+      $parser = new static($xpath, $prefix, new Lexer());
+      static::$cache[$prefix][$xpath] = $parser->parse();
+    }
+
+    return static::$cache[$prefix][$xpath];
+  }
+
+  /**
+   * Localizes an XPath expression.
+   *
+   * Converts an expression from //div/a to
+   * //*[local-name() = "div"]/*[local-name() = "a"].
+   *
+   * @param string $xpath
+   *   The XPath expression to prefix.
+   *
+   * @return string
+   *   The localized XPath expression.
+   */
+  public static function localize($xpath) {
+    return static::prefix($xpath, NULL);
+  }
+
+  /**
+   * Constructs a Namespacer object.
+   *
+   * @param string $expression
+   *   The XPath expression.
+   * @param string $prefix
+   *   The prefix to use.
+   * @param \Drupal\Component\XpathHelper\Lexer $lexer
+   *   The lexer that will produce tokens.
+   */
+  public function __construct($expression, $prefix, Lexer $lexer) {
+    $this->prefix = $prefix;
+    $this->tokens = $lexer->lex($expression);
+  }
+
+  /**
+   * Parses an XPath expression.
+   *
+   * @return string
+   *   The rewritten XPath expression.
+   */
+  public function parse() {
+    $output = '';
+
+    $token_count = count($this->tokens);
+
+    for ( ; $this->cursor < $token_count; $this->cursor++) {
+      $token = $this->tokens[$this->cursor];
+
+      // A token that should be copied directly to the output.
+      if ($this->shouldCopy($token)) {
+        $output .= $token;
+      }
+      // A namespaced element.
+      elseif ($element = $this->getNamespacedElement($token)) {
+        $output .= $element;
+      }
+      // Namespace the element.
+      else {
+        $output .= $this->rewrite($token);
+      }
+    }
+
+    return $output;
+  }
+
+  /**
+   * Rewrites the token.
+   *
+   * Either in the form prefix:element or *[local-name() = "element"]
+   *
+   * @param string $token
+   *   The element to rewrite.
+   *
+   * @return string
+   *   The rewritten string.
+   */
+  protected function rewrite($element) {
+    if ($this->prefix) {
+      return $this->prefix . ':' . $element;
+    }
+    return '*[local-name() = "' . $element . '"]';
+  }
+
+  /**
+   * Determines if a token should be copied as-is to the output.
+   *
+   * @param string $token
+   *   The token.
+   *
+   * @return bool
+   *   Returns true if the token should be copied, and false if not.
+   */
+  protected function shouldCopy($token) {
+    if (Lexer::isWordBoundary($token)) {
+      return TRUE;
+    }
+    // Attribute or quoted string.
+    elseif ($token[0] === '@' || $token[0] === '"' || $token[0] === "'") {
+      return TRUE;
+    }
+    elseif (is_numeric($token) || is_numeric($token[0])) {
+      return TRUE;
+    }
+    elseif ($this->isFunctionCall()) {
+      return TRUE;
+    }
+    elseif ($this->isOperator($token)) {
+      return TRUE;
+    }
+    elseif ($this->isAxis()) {
+      return TRUE;
+    }
+    elseif ($this->wasAttributeAxis()) {
+      return TRUE;
+    }
+    // Handles the edge case where subtraction is written like 2 - 1.
+    elseif ($token === '-') {
+      return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Returns the namespaced element.
+   *
+   * @param string $token
+   *   The token.
+   *
+   * @return string|bool
+   *   The namespaced element, or false if it doesn't exist.
+   */
+  protected function getNamespacedElement($token) {
+    if ($this->peek(1) !== ':') {
+      return FALSE;
+    }
+
+    // Build the namespaced element, prefix:element.
+    $token .= ':' . $this->peek(2);
+    $this->cursor += 2;
+    return $token;
+  }
+
+  /**
+   * Determines if the current token is a function call.
+   *
+   * @param string $token
+   *   The token.
+   *
+   * @return bool
+   *   Returns true if the token is a function call and false if not.
+   */
+  protected function isFunctionCall() {
+    // Spaces before the opening parens of a function call are valid.
+    // Ex: //div[contains   (@id, "thing")]
+    return $this->nextNonSpace() === '(';
+  }
+
+  /**
+   * Checks if a token is an operator, one of div, or, and, mod.
+   *
+   * @param string $token
+   *   The token to check.
+   *
+   * @return bool
+   *   Returns true if the token is an operator, and false if not.
+   */
+  protected function isOperator($token) {
+    if (!isset(static::$operators[$token])) {
+      return FALSE;
+    }
+
+    $prev = $this->prevNonSpace();
+    return $prev && !isset(static::$precedesElement[$prev]);
+  }
+
+  /**
+   * Determines whether this token is an axis.
+   *
+   * descendant-or-self, attribute, etc.
+   *
+   * @return bool
+   *   True if the token is an axis, false if not.
+   */
+  protected function isAxis() {
+    return $this->nextNonSpace() === '::';
+  }
+
+  /**
+   * Determines whether the preceding token was an attribute axis.
+   *
+   * attribute::
+   *
+   * @return bool
+   *   True if the preceding token was an attribute axis, false if not.
+   */
+  protected function wasAttributeAxis() {
+    return $this->prevNonSpace() === '::' && $this->prevNonSpace(2) === 'attribute';
+  }
+
+  /**
+   * Returns the next non-space token.
+   *
+   * @param int $delta
+   *   (optional) The delta of the next non-space character. Defaults to 1.
+   *
+   * @return string
+   *   The nth next non-space character.
+   */
+  protected function nextNonSpace($delta = 1) {
+    $n = 1;
+
+    for ($i = 0; $i < $delta; $i++) {
+      do {
+        $next = $this->peek($n);
+        $n++;
+      } while ($next === ' ');
+    }
+
+    return $next;
+  }
+
+  /**
+   * Returns the previous non-space token.
+   *
+   * @param int $delta
+   *   (optional) The delta of the previous non-space character. Defaults to 1.
+   *
+   * @return string
+   *   The nth previous non-space character.
+   */
+  protected function prevNonSpace($delta = 1) {
+    $n = -1;
+
+    for ($i = 0; $i < $delta; $i++) {
+      do {
+        $prev = $this->peek($n);
+        $n--;
+      } while ($prev === ' ');
+    }
+
+    return $prev;
+  }
+
+  /**
+   * Returns a token from an offset of the current position.
+   *
+   * @param int $offset
+   *   The offset from the current position.
+   *
+   * @return string
+   *   Returns the token at the given offset.
+   */
+  protected function peek($offset) {
+    return isset($this->tokens[$this->cursor + $offset]) ? $this->tokens[$this->cursor + $offset] : '';
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 01416ba..6efa7e2 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -150,7 +150,7 @@ public function label() {
       $label = call_user_func($label_callback, $this);
     }
     elseif (($label_key = $entity_type->getKey('label')) && isset($this->{$label_key})) {
-      $label = $this->{$label_key};
+      $label = (string) $this->{$label_key};
     }
     return $label;
   }
diff --git a/core/modules/aggregator/src/Tests/AggregatorRenderingTest.php b/core/modules/aggregator/src/Tests/AggregatorRenderingTest.php
index 351f48c..dc6cbe0 100644
--- a/core/modules/aggregator/src/Tests/AggregatorRenderingTest.php
+++ b/core/modules/aggregator/src/Tests/AggregatorRenderingTest.php
@@ -133,7 +133,7 @@ public function testFeedPage() {
     $outline = $this->xpath('//outline[1]');
     $this->assertEqual($outline[0]['type'], 'rss', 'The correct type attribute is used for rss OPML.');
     $this->assertEqual($outline[0]['text'], $feed->label(), 'The correct text attribute is used for rss OPML.');
-    $this->assertEqual($outline[0]['xmlurl'], $feed->getUrl(), 'The correct xmlUrl attribute is used for rss OPML.');
+    $this->assertEqual($outline[0]['xmlUrl'], $feed->getUrl(), 'The correct xmlUrl attribute is used for rss OPML.');
 
     // Check for the presence of a pager.
     $this->drupalGet('aggregator/sources/' . $feed->id());
diff --git a/core/modules/comment/src/Tests/Views/RowRssTest.php b/core/modules/comment/src/Tests/Views/RowRssTest.php
index 89b9c1b..4536652 100644
--- a/core/modules/comment/src/Tests/Views/RowRssTest.php
+++ b/core/modules/comment/src/Tests/Views/RowRssTest.php
@@ -31,7 +31,7 @@ public function testRssRow() {
     $result = $this->xpath('//item');
     $this->assertEqual(count($result), 1, 'Just one comment was found in the rss output.');
 
-    $this->assertEqual($result[0]->pubdate, gmdate('r', $this->comment->getCreatedTime()), 'The right pubDate appears in the rss output.');
+    $this->assertEqual($result[0]->pubDate, gmdate('r', $this->comment->getCreatedTime()), 'The right pubDate appears in the rss output.');
   }
 
 }
diff --git a/core/modules/config/src/Tests/ConfigEntityListTest.php b/core/modules/config/src/Tests/ConfigEntityListTest.php
index 9b74a5f..a5fde20 100644
--- a/core/modules/config/src/Tests/ConfigEntityListTest.php
+++ b/core/modules/config/src/Tests/ConfigEntityListTest.php
@@ -185,7 +185,7 @@ function testListUI() {
     // operations list.
     $this->assertIdentical((string) $elements[0], 'Default');
     $this->assertIdentical((string) $elements[1], 'dotted.default');
-    $this->assertTrue($elements[2]->children()->xpath('//ul'), 'Operations list found.');
+    $this->assertTrue($elements[2]->children()->xpath($this->localizeXpath('//ul')), 'Operations list found.');
 
     // Add a new entity using the operations link.
     $this->assertLink('Add test configuration');
diff --git a/core/modules/editor/editor.admin.inc b/core/modules/editor/editor.admin.inc
index 83bc2cf..67ebedf 100644
--- a/core/modules/editor/editor.admin.inc
+++ b/core/modules/editor/editor.admin.inc
@@ -92,8 +92,8 @@ function editor_image_upload_settings_form(Editor $editor) {
   $form['max_dimensions'] = array(
     '#type' => 'item',
     '#title' => t('Maximum dimensions'),
-    '#field_prefix' => '<div class="container-inline clearfix">',
-    '#field_suffix' => '</div>',
+    // '#field_prefix' => '<div class="container-inline clearfix">',
+    // '#field_suffix' => '</div>',
     '#description' => t('Images larger than these dimensions will be scaled down.'),
     '#states' => $show_if_image_uploads_enabled,
   );
diff --git a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
index dbccf77..7a8be2a 100644
--- a/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
+++ b/core/modules/image/src/Plugin/Field/FieldType/ImageItem.php
@@ -210,8 +210,8 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
       '#title' => t('Maximum image resolution'),
       '#element_validate' => array(array(get_class($this), 'validateResolution')),
       '#weight' => 4.1,
-      '#field_prefix' => '<div class="container-inline">',
-      '#field_suffix' => '</div>',
+      // '#field_prefix' => '<div class="container-inline">',
+      // '#field_suffix' => '</div>',
       '#description' => t('The maximum allowed image size expressed as WIDTH×HEIGHT (e.g. 640×480). Leave blank for no restriction. If a larger image is uploaded, it will be resized to reflect the given width and height. Resizing images on upload will cause the loss of <a href="@url">EXIF data</a> in the image.', array('@url' => 'http://en.wikipedia.org/wiki/Exchangeable_image_file_format')),
     );
     $element['max_resolution']['x'] = array(
@@ -237,8 +237,8 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
       '#title' => t('Minimum image resolution'),
       '#element_validate' => array(array(get_class($this), 'validateResolution')),
       '#weight' => 4.2,
-      '#field_prefix' => '<div class="container-inline">',
-      '#field_suffix' => '</div>',
+      // '#field_prefix' => '<div class="container-inline">',
+      // '#field_suffix' => '</div>',
       '#description' => t('The minimum allowed image size expressed as WIDTH×HEIGHT (e.g. 640×480). Leave blank for no restriction. If a smaller image is uploaded, it will be rejected.'),
     );
     $element['min_resolution']['x'] = array(
diff --git a/core/modules/image/src/Tests/ImageFieldDisplayTest.php b/core/modules/image/src/Tests/ImageFieldDisplayTest.php
index c4cf485..deb102d 100644
--- a/core/modules/image/src/Tests/ImageFieldDisplayTest.php
+++ b/core/modules/image/src/Tests/ImageFieldDisplayTest.php
@@ -315,8 +315,8 @@ function testImageFieldSettings() {
       'files[' . $field_name . '_2][]' => drupal_realpath($test_image->uri),
     );
     $this->drupalPostAjaxForm(NULL, $edit, $field_name . '_2_upload_button');
-    $this->assertNoRaw('<input multiple type="file" id="edit-' . strtr($field_name, '_', '-') . '-2-upload" name="files[' . $field_name . '_2][]" size="22" class="js-form-file form-file">');
-    $this->assertRaw('<input multiple type="file" id="edit-' . strtr($field_name, '_', '-') . '-3-upload" name="files[' . $field_name . '_3][]" size="22" class="js-form-file form-file">');
+    $this->assertNoRaw('<input multiple="multiple" type="file" id="edit-' . strtr($field_name, '_', '-') . '-2-upload" name="files[' . $field_name . '_2][]" size="22" class="js-form-file form-file">');
+    $this->assertRaw('<input multiple="multiple" type="file" id="edit-' . strtr($field_name, '_', '-') . '-3-upload" name="files[' . $field_name . '_3][]" size="22" class="js-form-file form-file">');
   }
 
   /**
diff --git a/core/modules/rdf/src/Tests/Field/FieldRdfaTestBase.php b/core/modules/rdf/src/Tests/Field/FieldRdfaTestBase.php
index cb8f584..da7df16 100644
--- a/core/modules/rdf/src/Tests/Field/FieldRdfaTestBase.php
+++ b/core/modules/rdf/src/Tests/Field/FieldRdfaTestBase.php
@@ -7,6 +7,7 @@
 namespace Drupal\rdf\Tests\Field;
 
 use Drupal\field\Tests\FieldUnitTestBase;
+use Masterminds\HTML5;
 
 abstract class FieldRdfaTestBase extends FieldUnitTestBase {
 
@@ -150,8 +151,8 @@ protected function getAbsoluteUri($entity) {
    *   An array containing simplexml objects.
    */
   protected function parseContent($content) {
-    $htmlDom = new \DOMDocument();
-    @$htmlDom->loadHTML('<?xml encoding="UTF-8">' . $content);
+    $html5 = new HTML5();
+    $htmlDom = $html5->loadHTML($content);
     $elements = simplexml_import_dom($htmlDom);
 
     return $elements;
@@ -177,6 +178,14 @@ protected function parseContent($content) {
   protected function xpathContent($content, $xpath, array $arguments = array()) {
     if ($elements = $this->parseContent($content)) {
       $xpath = $this->buildXPathQuery($xpath, $arguments);
+
+      // Register the default namespace if it exists.
+      $namespaces = $elements->getDocNamespaces();
+      if (!empty($namespaces[''])) {
+        $xpath = $this->prefixXpath($xpath);
+        $elements->registerXPathNamespace('x', $namespaces['']);
+      }
+
       $result = $elements->xpath($xpath);
       // Some combinations of PHP / libxml versions return an empty array
       // instead of the documented FALSE. Forcefully convert any falsish values
diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php
index 9ef0a66..a7b4b6d 100644
--- a/core/modules/simpletest/src/AssertContentTrait.php
+++ b/core/modules/simpletest/src/AssertContentTrait.php
@@ -12,6 +12,8 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Render\RenderContext;
+use Drupal\Component\XpathHelper\Namespacer;
+use Masterminds\HTML5;
 use Symfony\Component\CssSelector\CssSelector;
 
 /**
@@ -127,15 +129,22 @@ protected function setDrupalSettings($settings) {
    */
   protected function parse() {
     if (!isset($this->elements)) {
-      // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
-      // them.
-      $html_dom = new \DOMDocument();
-      @$html_dom->loadHTML('<?xml encoding="UTF-8">' . $this->getRawContent());
-      if ($html_dom) {
-        $this->pass(SafeMarkup::format('Valid HTML found on "@path"', array('@path' => $this->getUrl())), 'Browser');
+
+      // Check for XML preamble.
+      if (substr($this->getRawContent(), 0, 5) === '<?xml') {
+        $dom = new \DOMDocument();
+        $dom->loadXML($this->getRawContent());
+      }
+      else {
+        $html5 = new HTML5();
+        $dom = $html5->loadHTML($this->getRawContent());
+      }
+
+      if ($dom) {
+        $this->pass(SafeMarkup::format('Valid markup found on "@path"', array('@path' => $this->getUrl())), 'Browser');
         // It's much easier to work with simplexml than DOM, luckily enough
         // we can just simply import our DOM tree.
-        $this->elements = simplexml_import_dom($html_dom);
+        $this->elements = simplexml_import_dom($dom);
       }
     }
     if ($this->elements === FALSE) {
@@ -228,6 +237,14 @@ protected function buildXPathQuery($xpath, array $args = array()) {
   protected function xpath($xpath, array $arguments = array()) {
     if ($this->parse()) {
       $xpath = $this->buildXPathQuery($xpath, $arguments);
+
+      // Register the default namespace if it exists.
+      $namespaces = $this->elements->getDocNamespaces();
+      if (!empty($namespaces[''])) {
+        $xpath = $this->prefixXpath($xpath);
+        $this->elements->registerXPathNamespace('x', $namespaces['']);
+      }
+
       $result = $this->elements->xpath($xpath);
       // Some combinations of PHP / libxml versions return an empty array
       // instead of the documented FALSE. Forcefully convert any falsish values
@@ -240,6 +257,32 @@ protected function xpath($xpath, array $arguments = array()) {
   }
 
   /**
+   * Prefixes an xpath expression.
+   *
+   * @param string $xpath
+   *   The xpath expression.
+   *
+   * @return string
+   *   The prefixed xpath expression.
+   */
+  protected function prefixXpath($xpath) {
+    return Namespacer::prefix($xpath);
+  }
+
+  /**
+   * Localizes an xpath expression.
+   *
+   * @param string $xpath
+   *   The xpath expression.
+   *
+   * @return string
+   *   The localized xpath expression.
+   */
+  protected function localizeXpath($xpath) {
+    return Namespacer::localize($xpath);
+  }
+
+  /**
    * Searches elements using a CSS selector in the raw content.
    *
    * The search is relative to the root element (HTML tag normally) of the page.
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index c890beb..d3203d4 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -30,6 +30,7 @@
 use Drupal\Core\StreamWrapper\PublicStream;
 use Drupal\Core\Url;
 use Drupal\node\Entity\NodeType;
+use Masterminds\HTML5;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Zend\Diactoros\Uri;
@@ -1963,8 +1964,8 @@ protected function drupalProcessAjaxResponse($content, array $ajax_response, arr
     );
     // DOM can load HTML soup. But, HTML soup can throw warnings, suppress
     // them.
-    $dom = new \DOMDocument();
-    @$dom->loadHTML($content);
+    $html5 = new HTML5();
+    $dom = $html5->loadHTML($content);
     // XPath allows for finding wrapper nodes better than DOM does.
     $xpath = new \DOMXPath($dom);
     foreach ($ajax_response as $command) {
@@ -1989,14 +1990,11 @@ protected function drupalProcessAjaxResponse($content, array $ajax_response, arr
           //   and 'body', since these are used by
           //   \Drupal\Core\Ajax\AjaxResponse::ajaxRender().
           elseif (in_array($command['selector'], array('head', 'body'))) {
-            $wrapperNode = $xpath->query('//' . $command['selector'])->item(0);
+            $wrapperNode = $xpath->query($this->localizeXpath('//' . $command['selector']))->item(0);
           }
           if ($wrapperNode) {
             // ajax.js adds an enclosing DIV to work around a Safari bug.
-            $newDom = new \DOMDocument();
-            // DOM can load HTML soup. But, HTML soup can throw warnings,
-            // suppress them.
-            @$newDom->loadHTML('<div>' . $command['data'] . '</div>');
+            $newDom = $html5->loadHTML('<div>' . trim($command['data']) . '</div>');
             // Suppress warnings thrown when duplicate HTML IDs are encountered.
             // This probably means we are replacing an element with the same ID.
             $newNode = @$dom->importNode($newDom->documentElement->firstChild->firstChild, TRUE);
@@ -2048,14 +2046,14 @@ protected function drupalProcessAjaxResponse($content, array $ajax_response, arr
         case 'add_css':
           break;
         case 'update_build_id':
-          $buildId = $xpath->query('//input[@name="form_build_id" and @value="' . $command['old'] . '"]')->item(0);
+          $buildId = $xpath->query($this->localizeXpath('//input[@name="form_build_id" and @value="' . $command['old'] . '"]'))->item(0);
           if ($buildId) {
             $buildId->setAttribute('value', $command['new']);
           }
           break;
       }
     }
-    $content = $dom->saveHTML();
+    $content = $html5->saveHTML($dom);
     $this->setRawContent($content);
     $this->setDrupalSettings($drupal_settings);
   }
@@ -2204,12 +2202,13 @@ protected function cronRun() {
    */
   protected function checkForMetaRefresh() {
     if (strpos($this->getRawContent(), '<meta ') && $this->parse() && (!isset($this->maximumMetaRefreshCount) || $this->metaRefreshCount < $this->maximumMetaRefreshCount)) {
-      $refresh = $this->xpath('//meta[@http-equiv="Refresh"]');
-      if (!empty($refresh)) {
         // Parse the content attribute of the meta tag for the format:
         // "[delay]: URL=[page_to_redirect_to]".
-        if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $refresh[0]['content'], $match)) {
-          $this->metaRefreshCount++;
+      preg_match('|<meta\s*http-equiv\s*=\s*"Refresh"\s*content="(.*)"\s*/>|', $this->getRawContent(), $matches);
+      if ($matches) {
+        // Parse the content attribute of the meta tag for the format:
+        // "[delay]: URL=[page_to_redirect_to]".
+        if (preg_match('/\d+;\s*URL=(?<url>.*)/i', $matches[1], $match)) {
           return $this->drupalGet($this->getAbsoluteUrl(Html::decodeEntities($match['url'])));
         }
       }
@@ -2267,7 +2266,7 @@ protected function drupalHead($path, array $options = array(), array $headers =
    */
   protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
     // Retrieve the form elements.
-    $elements = $form->xpath('.//input[not(@disabled)]|.//textarea[not(@disabled)]|.//select[not(@disabled)]');
+    $elements = $form->xpath($this->localizeXpath('.//input[not(@disabled)]|.//textarea[not(@disabled)]|.//select[not(@disabled)]'));
     $submit_matches = FALSE;
     foreach ($elements as $element) {
       // SimpleXML objects need string casting all the time.
diff --git a/core/modules/system/src/Tests/Theme/FunctionsTest.php b/core/modules/system/src/Tests/Theme/FunctionsTest.php
index 44f4507..60b6eb6 100644
--- a/core/modules/system/src/Tests/Theme/FunctionsTest.php
+++ b/core/modules/system/src/Tests/Theme/FunctionsTest.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Session\UserSession;
 use Drupal\Core\Url;
 use Drupal\simpletest\WebTestBase;
+use Masterminds\HTML5;
 
 /**
  * Tests for common theme functions.
@@ -344,13 +345,14 @@ function testDrupalPreRenderLinks() {
       ),
     );
 
+    $html5 = new HTML5();
+
     // Start with a fresh copy of the base array, and try rendering the entire
     // thing. We expect a single <ul> with appropriate links contained within
     // it.
     $render_array = $base_array;
     $html = \Drupal::service('renderer')->renderRoot($render_array);
-    $dom = new \DOMDocument();
-    $dom->loadHTML($html);
+    $dom = $html5->loadHTML($html);
     $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, 'One "ul" tag found in the rendered HTML.');
     $list_elements = $dom->getElementsByTagName('li');
     $this->assertEqual($list_elements->length, 3, 'Three "li" tags found in the rendered HTML.');
@@ -367,16 +369,14 @@ function testDrupalPreRenderLinks() {
     $child_html = \Drupal::service('renderer')->renderRoot($render_array['first_child']);
     $parent_html = \Drupal::service('renderer')->renderRoot($render_array);
     // First check the child HTML.
-    $dom = new \DOMDocument();
-    $dom->loadHTML($child_html);
+    $dom = $html5->loadHTML($child_html);
     $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, 'One "ul" tag found in the rendered child HTML.');
     $list_elements = $dom->getElementsByTagName('li');
     $this->assertEqual($list_elements->length, 2, 'Two "li" tags found in the rendered child HTML.');
     $this->assertEqual($list_elements->item(0)->nodeValue, 'Parent link copy', 'First expected link found.');
     $this->assertEqual($list_elements->item(1)->nodeValue, 'First child link', 'Second expected link found.');
     // Then check the parent HTML.
-    $dom = new \DOMDocument();
-    $dom->loadHTML($parent_html);
+    $dom = $html5->loadHTML($parent_html);
     $this->assertEqual($dom->getElementsByTagName('ul')->length, 1, 'One "ul" tag found in the rendered parent HTML.');
     $list_elements = $dom->getElementsByTagName('li');
     $this->assertEqual($list_elements->length, 2, 'Two "li" tags found in the rendered parent HTML.');
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestCheckboxesZeroForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestCheckboxesZeroForm.php
index ccd752f..e7d0aa6 100644
--- a/core/modules/system/tests/modules/form_test/src/Form/FormTestCheckboxesZeroForm.php
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestCheckboxesZeroForm.php
@@ -56,7 +56,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $json = T
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
-    if ($form_state->has('json')) {
+    if ($form_state->get('json')) {
       $form_state->setResponse(new JsonResponse($form_state->getValues()));
     }
     else {
diff --git a/core/modules/views/src/Tests/Handler/FieldWebTest.php b/core/modules/views/src/Tests/Handler/FieldWebTest.php
index 98fe8af..4087211 100644
--- a/core/modules/views/src/Tests/Handler/FieldWebTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldWebTest.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Render\RenderContext;
 use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
 use Drupal\views\Views;
+use Masterminds\HTML5;
 
 /**
  * Tests fields from within a UI.
@@ -154,11 +155,9 @@ protected function assertNotSubString($haystack, $needle, $message = '', $group
    *   An array containing simplexml objects.
    */
   protected function parseContent($content) {
-    $htmlDom = new \DOMDocument();
-    @$htmlDom->loadHTML('<?xml encoding="UTF-8">' . $content);
-    $elements = simplexml_import_dom($htmlDom);
-
-    return $elements;
+    $html5 = new HTML5();
+    $dom = $html5->loadHTML($content);
+    return @simplexml_import_dom($dom);
   }
 
   /**
@@ -181,6 +180,14 @@ protected function parseContent($content) {
   protected function xpathContent($content, $xpath, array $arguments = array()) {
     if ($elements = $this->parseContent($content)) {
       $xpath = $this->buildXPathQuery($xpath, $arguments);
+
+      // Register the default namespace if it exists.
+      $namespaces = $elements->getDocNamespaces();
+      if (!empty($namespaces[''])) {
+        $xpath = $this->prefixXpath($xpath);
+        $elements->registerXPathNamespace('x', $namespaces['']);
+      }
+
       $result = $elements->xpath($xpath);
       // Some combinations of PHP / libxml versions return an empty array
       // instead of the documented FALSE. Forcefully convert any falsish values
diff --git a/core/modules/views/src/Tests/Plugin/StyleOpmlTest.php b/core/modules/views/src/Tests/Plugin/StyleOpmlTest.php
index 6580c0c..df008c5 100644
--- a/core/modules/views/src/Tests/Plugin/StyleOpmlTest.php
+++ b/core/modules/views/src/Tests/Plugin/StyleOpmlTest.php
@@ -60,7 +60,7 @@ public function testOpmlOutput() {
     $outline = $this->xpath('//outline[1]');
     $this->assertEqual($outline[0]['type'], 'rss', 'The correct type attribute is used for rss OPML.');
     $this->assertEqual($outline[0]['text'], $feed->label(), 'The correct text attribute is used for rss OPML.');
-    $this->assertEqual($outline[0]['xmlurl'], $feed->getUrl(), 'The correct xmlUrl attribute is used for rss OPML.');
+    $this->assertEqual($outline[0]['xmlUrl'], $feed->getUrl(), 'The correct xmlUrl attribute is used for rss OPML.');
 
     $view = $this->container->get('entity.manager')
       ->getStorage('view')
diff --git a/core/modules/views/src/Tests/Plugin/StyleTest.php b/core/modules/views/src/Tests/Plugin/StyleTest.php
index 68ce82e..1d817d7 100644
--- a/core/modules/views/src/Tests/Plugin/StyleTest.php
+++ b/core/modules/views/src/Tests/Plugin/StyleTest.php
@@ -13,6 +13,7 @@
 use Drupal\views\Plugin\views\row\Fields;
 use Drupal\views\ResultRow;
 use Drupal\views_test_data\Plugin\views\style\StyleTest as StyleTestPlugin;
+use Masterminds\HTML5;
 
 /**
  * Tests general style functionality.
@@ -287,8 +288,8 @@ function testCustomRowClasses() {
    * Stores a view output in the elements.
    */
   protected function storeViewPreview($output) {
-    $htmlDom = new \DOMDocument();
-    @$htmlDom->loadHTML($output);
+    $html5 = new HTML5();
+    $htmlDom = $html5->loadHTML('<html><body>' . $output . '</body></html>');
     if ($htmlDom) {
       // It's much easier to work with simplexml than DOM, luckily enough
       // we can just simply import our DOM tree.
diff --git a/core/tests/Drupal/Tests/Component/XpathHelper/LexerTest.php b/core/tests/Drupal/Tests/Component/XpathHelper/LexerTest.php
new file mode 100644
index 0000000..4c4ca78
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/XpathHelper/LexerTest.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Component\XpathHelper\LexerTest.
+ */
+
+namespace Drupal\Tests\Component\XpathHelper;
+
+use Drupal\Component\XpathHelper\Lexer;
+
+/**
+ * @coversDefaultClass \Drupal\Component\XpathHelper\Lexer
+ * @group XpathHelper
+ */
+class LexerTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * @covers ::lex
+   * @dataProvider providerLex
+   */
+  public function testLex($input, $expected) {
+    $lexer = new Lexer();
+    $this->assertSame($expected, $lexer->lex($input));
+  }
+
+  /**
+   * Data provider for testLex().
+   *
+   * @return array
+   *   - An xpath argument to lex().
+   *   - Expected output from lex().
+   */
+  public function providerLex() {
+    return [
+      ['cat', ['cat']],
+      ['/cow/barn', ['/', 'cow', '/', 'barn']],
+      ['""', ['""']],
+      ['/cow/barn[@id = "asdfsaf"]', ['/', 'cow', '/', 'barn', '[', '@id', ' ', '=', ' ', '"asdfsaf"', ']']],
+      ['/cow/barn[@id=chair]', ['/', 'cow', '/', 'barn', '[', '@id', '=', 'chair', ']']],
+      ['/cow:asdf', ['/', 'cow', ':', 'asdf']],
+      ['@cow', ['@cow']],
+      ['starts-with(@id, "cat")', ['starts-with', '(', '@id' , ',', ' ', '"cat"', ')']],
+      ['starts-with(cat/dog/fire:breather, "cat")', ['starts-with', '(', 'cat', '/', 'dog', '/', 'fire' , ':', 'breather', ',', ' ', '"cat"', ')']],
+      ['child::book', ['child', '::', 'book']],
+      ["//a[@href='javascript:void(0)']", ['//', 'a', '[', '@href', '=', "'javascript:void(0)'", ']']],
+      ['1+1', ['1', '+', '1']],
+      ['//a[@id="id"and 1]', ['//', 'a', '[', '@id', '=', '"id"', 'and', ' ', '1', ']']],
+      ['0', ['0']],
+    ];
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/Component/XpathHelper/NamespacerTest.php b/core/tests/Drupal/Tests/Component/XpathHelper/NamespacerTest.php
new file mode 100644
index 0000000..fa02cc8
--- /dev/null
+++ b/core/tests/Drupal/Tests/Component/XpathHelper/NamespacerTest.php
@@ -0,0 +1,310 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\Component\XpathHelper\NamespacerTest.
+ */
+
+namespace Drupal\Tests\Component\XpathHelper;
+
+use Drupal\Component\XpathHelper\Namespacer;
+
+/**
+ * @coversDefaultClass \Drupal\Component\XpathHelper\Namespacer
+ * @group XpathHelper
+ */
+class NamespacerTest extends \PHPUnit_Framework_TestCase {
+
+  /**
+   * Data provider for testLocalize().
+   *
+   * @return array
+   *   - Expected output from localize().
+   *   - An xpath argument to localize().
+   */
+  public function providerLocalize() {
+    return [
+      ['*[local-name() = "a"]', 'a'],
+    ];
+  }
+
+  /**
+   * @covers ::localize
+   * @dataProvider providerLocalize
+   */
+  public function testLocalize($expected, $xpath) {
+    $this->assertSame($expected, Namespacer::localize($xpath));
+  }
+
+  /**
+   * @covers ::prefix
+   * @dataProvider providerPrefixDefaultPrefix
+   */
+  public function testPrefixDefaultPrefix($xpath, $expected) {
+    $this->assertSame($expected, Namespacer::prefix($xpath));
+  }
+
+  /**
+   * Data provider for testPrefixDefaultPrefix().
+   *
+   * Gives us data which we can use to test prefix()'s default prefix.
+   *
+   * @return array
+   *   - An xpath in need of a prefix.
+   *   - The expected xpath with the default prefix added.
+   */
+  public function providerPrefixDefaultPrefix() {
+    $tests = [
+      ['cow', 'x:cow'],
+      ['/cow/barn', '/x:cow/x:barn'],
+      ['/cow/barn[@id = "asdfsaf"]', '/x:cow/x:barn[@id = "asdfsaf"]'],
+      ['/cow/barn [@id = "asdfsaf"]', '/x:cow/x:barn [@id = "asdfsaf"]'],
+      ['/cow/barn[@id=chair]', '/x:cow/x:barn[@id=x:chair]'],
+      ['/cow:asdf', '/cow:asdf'],
+      ['@cow', '@cow'],
+      ['starts-with(@id, "cat")', 'starts-with(@id, "cat")'],
+      ['starts-with(cat/dog/fire:breather, "cat")', 'starts-with(x:cat/x:dog/fire:breather, "cat")'],
+      ['//state[@id = ../city[name="CityName"]/state_id]/name', '//x:state[@id = ../x:city[x:name="CityName"]/x:state_id]/x:name'],
+      ['attribute::lang', 'attribute::lang'],
+      ['attribute:: lang', 'attribute:: lang'],
+      ['attribute ::lang', 'attribute ::lang'],
+      ['attribute :: lang', 'attribute :: lang'],
+      ['child::book', 'child::x:book'],
+      ['child :: book', 'child :: x:book'],
+      ['child::*', 'child::*'],
+      ['child:: *', 'child:: *'],
+      ['child ::*', 'child ::*'],
+      ['child :: *', 'child :: *'],
+      ['child::text()', 'child::text()'],
+      ['child::text   ()', 'child::text   ()'],
+      ['ancestor-or-self::book', 'ancestor-or-self::x:book'],
+      ['child::*/child::price', 'child::*/child::x:price'],
+      ["/asdfasfd[@id = 'a' or @id='b']", "/x:asdfasfd[@id = 'a' or @id='b']"],
+      ["id('yui-gen2')/x:div[3]/x:div/x:a[1]", "id('yui-gen2')/x:div[3]/x:div/x:a[1]"],
+      ["/descendant::a[@class='buttonCheckout']", "/descendant::x:a[@class='buttonCheckout']"],
+      ["//a[@href='javascript:void(0)']", "//x:a[@href='javascript:void(0)']"],
+      ['//*/@attribute', '//*/@attribute'],
+      ['/descendant::*[attribute::attribute]', '/descendant::*[attribute::attribute]'],
+      ['//Event[not(System/Level = preceding::Level) or not(System/Task = preceding::Task)]', '//x:Event[not(x:System/x:Level = preceding::x:Level) or not(x:System/x:Task = preceding::x:Task)]'],
+      ["section[@type='cover']/line/@page", "x:section[@type='cover']/x:line/@page"],
+      ['/articles/article/*[name()="title" or name()="short"]', '/x:articles/x:article/*[name()="title" or name()="short"]'],
+      ["/*/article[@id='2']/*[self::title or self::short]", "/*/x:article[@id='2']/*[self::x:title or self::x:short]"],
+      ['not(/asdfasfd/asdfasf//asdfasdf) | /asdfasf/sadfasf/@asdf', 'not(/x:asdfasfd/x:asdfasf//x:asdfasdf) | /x:asdfasf/x:sadfasf/@asdf'],
+      ['Ülküdak', 'x:Ülküdak'],
+      ['//textarea[@name="style[type]"]|//input[@name="style[type]"]|//select[@name="style[type]"]', '//x:textarea[@name="style[type]"]|//x:input[@name="style[type]"]|//x:select[@name="style[type]"]'],
+      ['//a[@id="id"and 1]', '//x:a[@id="id"and 1]'],
+      ['//*[@id and@class]', '//*[@id and@class]'],
+      ['/or', '/x:or'],
+      ['//and', '//x:and'],
+      ['div', 'x:div'],
+      ['a-1', 'x:a-1'],
+      ['//element [contains(@id, "1234")and contains(@id, 345)]', '//x:element [contains(@id, "1234")and contains(@id, 345)]'],
+      ['following-sibling::div', 'following-sibling::x:div'],
+      ['//  div  /  a  /  @href', '//  x:div  /  x:a  /  @href'],
+      ['a[contains(div, div)', 'x:a[contains(x:div, x:div)'],
+    ];
+
+    // Math related.
+    foreach (['+', '-', '*', '=', '!=', '<', '>', '<=', '>='] as $op) {
+      $tests[] = ["1{$op}2", "1{$op}2"];
+      $tests[] = ["1 {$op}2", "1 {$op}2"];
+      $tests[] = ["1{$op} 2", "1{$op} 2"];
+      $tests[] = ["1 {$op} 2", "1 {$op} 2"];
+    }
+
+    foreach (['and', 'or', 'mod', 'div'] as $op) {
+      $tests[] = ["1{$op} 2", "1{$op} 2"];
+      $tests[] = ["1 {$op} 2", "1 {$op} 2"];
+    }
+
+    return $tests;
+  }
+
+  /**
+   * Data provider for testParse().
+   *
+   * @return array
+   *   - Expected parsed text.
+   *   - Array of tokens to parse.
+   *   - Return value for mocked shouldCopy().
+   *   - Return value for mocked getNamespacedElement(). FALSE means return
+   *     false, while any other value means return the string 'namespaced'.
+   */
+  public function providerParse() {
+    return [
+      ['', [], FALSE, FALSE],
+      ['', [], TRUE, TRUE],
+      ['token', ['token'], TRUE, TRUE],
+      ['tokentoken', ['token', 'token'], TRUE, TRUE],
+      ['namespaced', ['token'], FALSE, TRUE],
+      ['namespacednamespaced', ['token', 'token'], FALSE, TRUE],
+      ['rewritten', ['token'], FALSE, FALSE],
+      ['rewrittenrewritten', ['token', 'token'], FALSE, FALSE],
+    ];
+  }
+
+  /**
+   * @covers ::parse
+   * @dataProvider providerParse
+   */
+  public function testParse($expected, $token_array, $should_copy, $get_namespaced_element) {
+    // Create a mocked Namespacer object.
+    $mock_namespacer = $this->getMockBuilder('\Drupal\Component\XpathHelper\Namespacer')
+      ->disableOriginalConstructor()
+      ->setMethods(array('shouldCopy', 'getNamespacedElement', 'rewrite'))
+      ->getMock();
+
+    // Set expectations for shouldCopy(). It gets called on every token in the
+    // array.
+    $mock_namespacer->expects($this->exactly(count($token_array)))
+      ->method('shouldCopy')
+      ->willReturn($should_copy);
+
+    // Set expectations for getNamespacedElement(). It's called for any token
+    // in the array that shouldn't copy. We'll can just use $shouldCopy
+    // to turn expectations on or off.
+    $get_namespaced_element_count = 0;
+    if (!$should_copy) {
+      $get_namespaced_element_count = count($token_array);
+    }
+    // If getNamespacedElement() returns FALSE, we'll call rewrite(), so we have
+    // to manage that.
+    $get_namespaced_element_value = $get_namespaced_element;
+    if ($get_namespaced_element) {
+      $get_namespaced_element_value = 'namespaced';
+    }
+    // Finally assemble all this in the method.
+    $mock_namespacer->expects($this->exactly($get_namespaced_element_count))
+      ->method('getNamespacedElement')
+      ->willReturn($get_namespaced_element_value);
+
+    // Set expectations for rewrite().
+    $rewrite_count = 0;
+    if (!$should_copy && !$get_namespaced_element) {
+      $rewrite_count = count($token_array);
+    }
+    $mock_namespacer->expects($this->exactly($rewrite_count))
+      ->method('rewrite')
+      ->willReturn('rewritten');
+
+    // Set $tokens. $tokens is protected so we must use reflection.
+    $ref_tokens = new \ReflectionProperty($mock_namespacer, 'tokens');
+    $ref_tokens->setAccessible(TRUE);
+    $ref_tokens->setValue($mock_namespacer, $token_array);
+
+    // Set $cursor so our parse always starts from the beginning.
+    $ref_cursor = new \ReflectionProperty($mock_namespacer, 'cursor');
+    $ref_cursor->setAccessible(TRUE);
+    $ref_cursor->setValue($mock_namespacer, 0);
+
+    // Exercise parse().
+    $this->assertEquals($expected, $mock_namespacer->parse());
+  }
+
+  /**
+   * Data provider for testShouldCopy().
+   *
+   * The expectation of how many times a method will be called is encoded.
+   * Positive numbers are how many time the method will be called and will
+   * return TRUE. Negative numbers are the number of times the method will be
+   * callled and return FALSE.
+   *
+   * @return array
+   *   - Expected bool result.
+   *   - Token to test against.
+   *   - (optional) Integer expectation of how many times isFunctionCall() will
+   *     be called.
+   *   - (optional) Integer expectation of how many times isOperator() will be
+   *     called.
+   *   - (optional) Integer expectation of how many times isAxis() will be
+   *     called.
+   *   - (optional) Integer expectation of how many times wasAttributeAxis()
+   *     will be called.
+   */
+  public function providerShouldCopy() {
+    return [
+      // Values for Lexer::isWordBoundary().
+      [TRUE, '['],
+      [TRUE, '['],
+      [TRUE, ']'],
+      [TRUE, '='],
+      [TRUE, '('],
+      [TRUE, ')'],
+      [TRUE, '.'],
+      [TRUE, '<'],
+      [TRUE, '>'],
+      [TRUE, '*'],
+      [TRUE, '+'],
+      [TRUE, '!'],
+      [TRUE, '|'],
+      [TRUE, ','],
+      [TRUE, ' '],
+      [TRUE, '"'],
+      [TRUE, "'"],
+      [TRUE, ':'],
+      [TRUE, '::'],
+      [TRUE, '/'],
+      [TRUE, '//'],
+      [TRUE, '@'],
+      [TRUE, '@attribute'],
+      [TRUE, '"quoted"'],
+      [TRUE, "'quoted'"],
+      // Numeric.
+      [TRUE, '5'],
+      [TRUE, '23skidoo'],
+      // These hit our various method calls. They are named after the methods
+      // they hit, which happen to also be tokens that would return FALSE from
+      // shouldCopy().
+      [TRUE, 'isFunctionCall', 1],
+      [TRUE, 'isOperator', -1, 1],
+      [TRUE, 'isAxis', -1, -1, 1],
+      [TRUE, 'wasAttributeAxis', -1, -1, -1, 1],
+      // Special case for minus.
+      [TRUE, '-', -1, -1, -1, -1],
+      [FALSE, 'a token which should not be copied', -1, -1, -1, -1],
+    ];
+  }
+
+  /**
+   * @covers ::shouldCopy
+   * @dataProvider providerShouldCopy
+   */
+  public function testShouldCopy($expected, $token, $is_function_call = 0, $is_operator = 0, $is_axis = 0, $was_attribute_axis = 0) {
+    // Create a mocked Namespacer object.
+    $mock_namespacer = $this->getMockBuilder('\Drupal\Component\XpathHelper\Namespacer')
+      ->disableOriginalConstructor()
+      ->setMethods(['isFunctionCall', 'isOperator', 'isAxis', 'wasAttributeAxis'])
+      ->getMock();
+
+    // Set expectations for the dependency methods. The various parameters map
+    // out our expectations. The expectation of how many times a method will be
+    // called is encoded. Positive numbers are how many time the method will be
+    // called and will return TRUE. Negative numbers are the number of times the
+    // method will be callled and return FALSE.
+    $method_expectations_array = [
+      'isFunctionCall' => $is_function_call,
+      'isOperator' => $is_operator,
+      'isAxis' => $is_axis,
+      'wasAttributeAxis' => $was_attribute_axis,
+    ];
+    foreach ($method_expectations_array as $method => $expectation) {
+      $count = abs($expectation);
+      // Positive expectations return TRUE, negative expectations return FALSE.
+      $value = $expectation > 0;
+      $mock_namespacer->expects($this->exactly($count))
+        ->method($method)
+        ->willReturn($value);
+    }
+
+    // Since shouldCopy() is protected, we have to un-protect it.
+    $ref_should_copy = new \ReflectionMethod($mock_namespacer, 'shouldCopy');
+    $ref_should_copy->setAccessible(TRUE);
+
+    // Finally exercise shouldCopy().
+    $this->assertSame(
+      $expected,
+      $ref_should_copy->invoke($mock_namespacer, $token)
+    );
+  }
+
+}
