protected static function replacePHP(&$php)
	{
		// Expression that matches a $node->getAttribute() call and captures its string argument
		$getAttribute = "\\\$node->getAttribute\\(('[^']+')\\)";

		// Expression that matches a single-quoted string literal
		$string       = "'(?:[^\\\\']|\\\\.)*+'";

		$replacements = [
			'$this->out' => '$html',

			// An attribute value escaped as ENT_NOQUOTES. We only need to unescape quotes
			'(htmlspecialchars\\(' . $getAttribute . ',' . ENT_NOQUOTES . '\\))'
				=> "str_replace('&quot;','\"',\$attributes[\$1])",

			// One or several attribute values escaped as ENT_COMPAT can be used as-is
			'(htmlspecialchars\\((' . $getAttribute . '(?:\\.' . $getAttribute . ')*),' . ENT_COMPAT . '\\))'
				=> function ($m) use ($getAttribute)
				{
					return preg_replace('(' . $getAttribute . ')', '$attributes[$1]', $m[1]);
				},

			// Character replacement can be performed directly on the escaped value provided that it
			// is then escaped as ENT_COMPAT and that replacements do not interfere with the escaping
			// of the characters &<>" or their representation &amp;&lt;&gt;&quot;
			'(htmlspecialchars\\(strtr\\(' . $getAttribute . ",('[^\"&\\\\';<>aglmopqtu]+'),('[^\"&\\\\'<>]+')\\)," . ENT_COMPAT . '\\))'
				=> 'strtr($attributes[$1],$2,$3)',

			// A comparison between two attributes. No need to unescape
			'(' . $getAttribute . '(!?=+)' . $getAttribute . ')'
				=> '$attributes[$1]$2$attributes[$3]',

			// A comparison between an attribute and a literal string. Rather than unescape the
			// attribute value, we escape the literal. This applies to comparisons using XPath's
			// contains() as well (translated to PHP's strpos())
			'(' . $getAttribute . '===(' . $string . '))s'
				=> function ($m)
				{
					return '$attributes[' . $m[1] . ']===' . htmlspecialchars($m[2], ENT_COMPAT);
				},

			'((' . $string . ')===' . $getAttribute . ')s'
				=> function ($m)
				{
					return htmlspecialchars($m[1], ENT_COMPAT) . '===$attributes[' . $m[2] . ']';
				},

			'(strpos\\(' . $getAttribute . ',(' . $string . ')\\)([!=]==(?:0|false)))s'
				=> function ($m)
				{
					return 'strpos($attributes[' . $m[1] . "]," . htmlspecialchars($m[2], ENT_COMPAT) . ')' . $m[3];
				},

			'(strpos\\((' . $string . '),' . $getAttribute . '\\)([!=]==(?:0|false)))s'
				=> function ($m)
				{
					return 'strpos(' . htmlspecialchars($m[1], ENT_COMPAT) . ',$attributes[' . $m[2] . '])' . $m[3];
				},

			// An attribute value used in an arithmetic comparison or operation does not need to be
			// unescaped. The same applies to empty(), isset() and conditionals
			'(' . $getAttribute . '(?=(?:==|[-+*])\\d+))'  => '$attributes[$1]',
			'(\\b(\\d+(?:==|[-+*]))' . $getAttribute . ')' => '$1$attributes[$2]',
			'(empty\\(' . $getAttribute . '\\))'           => 'empty($attributes[$1])',
			"(\\\$node->hasAttribute\\(('[^']+')\\))"      => 'isset($attributes[$1])',
			'if($node->attributes->length)'                => 'if($this->hasNonNullValues($attributes))',

			// In all other situations, unescape the attribute value before use
			'(' . $getAttribute . ')' => 'htmlspecialchars_decode($attributes[$1])'
		];

		foreach ($replacements as $match => $replace)
		{
			if ($replace instanceof Closure)
			{
				$php = preg_replace_callback($match, $replace, $php);
			}
			elseif ($match[0] === '(')
			{
				$php = preg_replace($match, $replace, $php);
			}
			else
			{
				$php = str_replace($match, $replace, $php);
			}
		}
	}