Interpreters\PostType::class, 'current' => Interpreters\CurrentPost::class, 'current-date-time' => Interpreters\CurrentDateTime::class, 'site-title' => Interpreters\SiteTitle::class, 'site-tagline' => Interpreters\SiteTagline::class, 'user-info' => Interpreters\UserInfo::class, 'request-parameter' => Interpreters\RequestParams::class, 'shortcode' => Interpreters\Shortcode::class, 'link' => Interpreters\Link::class, 'image' => Interpreters\Image::class, ]; protected string $content; protected array $context; public function __construct( string $content, $context = [] ) { $this->content = $content; $this->context = $context; $this->parse();// exit; } public function parse() : void { $source = $this->content; // Matches arrive left to right, so the HTML context is tracked once // across the block rather than rescanned from the start for each tag. $scan = [ 'pos' => 0, 'mode' => 'text', 'quote' => '', ]; $this->content = preg_replace_callback( '~ablocks_dc:(.+?):ablocks_dc|]*\bclass=["\']ablocks-richtext-dynamic-content["\'])(?=[^>]*\bdata-source=["\']([^"\']+)["\'])(?=[^>]*\bdata-field=["\']([^"\']+)["\'])[^>]*>(.*?)<\/span>~ims', function ( $matches ) use ( $source, &$scan ) { // Offsets are captured only to answer one question — is this // tag inside a tag's attributes or in text — and are then // dropped, so apply_changes() sees the shape it always has. $offset = $matches[0][1]; $plain = array_map( function ( $match ) { return is_array( $match ) ? $match[0] : $match; }, $matches ); // Trailing groups that did not participate come back as // [ '', -1 ]; the non-offset call omitted them entirely. while ( count( $plain ) > 1 && '' === end( $plain ) && -1 === $matches[ count( $plain ) - 1 ][1] ) { array_pop( $plain ); } $in_attribute = self::is_inside_tag( $source, $offset, $scan ); $url_start = $in_attribute && self::starts_url_attribute( $source, $offset ); // A tag that is also in this request came from the visitor — // a search box, a page echoing a query arg — not the author. if ( self::is_reflected( $plain[0] ) ) { return self::neutralize( $plain[0] ); } $output = $this->apply_changes( $plain, $in_attribute ); // esc_attr() keeps `javascript:`; a tag that opens a URL // attribute decides its scheme, so drop unsafe ones. return $url_start ? wp_kses_bad_protocol( $output, wp_allowed_protocols() ) : $output; }, $this->content, -1, $count, PREG_OFFSET_CAPTURE ); } public function apply_changes( array $matches, bool $in_attribute = false ): string { $filtered = $this->filter( $matches ); // returns an array $args = array_merge( $filtered, [ $this->interpreters, $this->context ] ); $parser = new ArgumentParser( ...$args ); $ins = $parser->interpret(); if ( is_null( $ins ) ) { return ''; } $ins->set_in_attribute( $in_attribute ); return self::neutralize( $ins->content() ); } /** * Whether a position in an HTML string falls inside a tag's attributes. * * Walks the HTML from where the previous call stopped, tracking quoted * attribute values, so a `>` or `<` inside `title="a>b"` does not end or * start a tag. Anything unclear counts as an attribute, the stricter of * the two escapes. * * @param string $html The block's HTML. * @param int $offset Byte offset of the tag; never less than the last call's. * @param array $state Scanner state carried between calls. * @return bool */ private static function is_inside_tag( string $html, int $offset, array &$state ) : bool { $i = $state['pos']; while ( $i < $offset ) { if ( 'comment' === $state['mode'] ) { $end = strpos( $html, '-->', $i ); if ( false === $end || $end + 3 > $offset ) { $i = $offset; break; } $i = $end + 3; $state['mode'] = 'text'; } elseif ( 'tag' === $state['mode'] ) { $i += strcspn( $html, '' === $state['quote'] ? '"\'>' : $state['quote'], $i, $offset - $i ); if ( $i >= $offset ) { break; } $char = $html[ $i ]; if ( '' !== $state['quote'] ) { $state['quote'] = ''; } elseif ( '>' === $char ) { $state['mode'] = 'text'; } else { $state['quote'] = $char; } ++$i; } else { $i += strcspn( $html, '<', $i, $offset - $i ); if ( $i >= $offset ) { break; } if ( 0 === substr_compare( $html, '