*/
private array $format_offsets = array();
/**
* Relates an index in the {@see self::$format_offsets} array to a format in
* the {@see self::$formats} array; used to asosociate start and end of formats.
*
* @see self::$formats
*
* @var Array
*/
private array $format_indices = array();
private array $open_formats = array();
private bool $contains_non_whitespace = false;
/**
* References to inline formats and their state.
*
* @see self::$format_offset
* @see self::$format_indices
*
* @var array
*/
private array $formats = array();
public function append_text( string $text ) {
$this->buffer .= $text;
if ( ! $this->contains_non_whitespace ) {
$this->contains_non_whitespace = \strspn( $text, " \t\n" ) !== \strlen( $text );
}
}
public function require_format( WP_Experimental_HTML_Renderer_Format $format ) {
$next_format_at = \count( $this->formats );
$this->open_formats[] = $next_format_at;
$this->format_offsets[] = \strlen( $this->buffer );
$this->format_indices[] = $next_format_at;
$this->formats[] = $format;
}
public function release_format() {
$this->format_offsets[] = -\strlen( $this->buffer );
/*
* Sometimes, the line buffer has been replaced by the time that the
* format is released. What could cause this to happen?
*/
// assert( count( $this->open_formats ) > 0 );
$this->format_indices[] = \array_pop( $this->open_formats );
}
public function raw_buffer(): string {
return $this->buffer;
}
public function flush( WP_Experimental_HTML_Renderer_Options $options ): string {
$offsets = $this->format_offsets;
$indices = $this->format_indices;
$formats = $this->formats;
$was_at = 0;
$buffer = '';
$length = \strlen( $this->buffer );
/** @var Array $effects */
$effects = array(
'bolding' => 0,
'emphasizing' => 0,
'monospacing' => 0,
'newlining' => 0,
'quoting' => 0,
'striking-out' => 0,
'subscripting' => 0,
'superscripting' => 0,
);
$syntax = array(
'bolding' => array( '**' ),
'emphasizing' => array( '_' ),
'monospacing' => array( '`' ),
'newlining' => array( "\n" ),
'quoting' => array( '“', '”', '‘', '’' ),
'striking-out' => array( '~' ),
);
$replacements = array(
'bolding' => array( '*' => '\*' ),
'emphasizing' => array( '_' => '\_' ),
'monospacing' => array( '`' => '\`' ),
'striking-out' => array( '~' => '\~' ),
);
for ( $i = 0; $i < \count( $offsets ); $i++ ) {
$at = $offsets[ $i ];
$state = $at < 0 ? 'exiting' : 'entering';
$at = \abs( $at );
$index = $indices[ $i ];
// @todo Why is this necessary?
if ( ! isset( $index ) ) {
goto next;
}
$format = $formats[ $index ];
if ( $at > $was_at ) {
$chunk = \substr( $this->buffer, $was_at, $at - $was_at );
foreach ( $effects as $effect => $depth ) {
if ( $depth > 0 && isset( $replacements[ $effect ] ) ) {
$chunk = \strtr( $chunk, $replacements[ $effect ] );
}
}
$buffer .= $chunk;
}
if ( $format instanceof WP_Experimental_HTML_Renderer_Format_Generic ) {
$type = $format->type;
$depth = $effects[ $type ];
if ( 'quoting' === $type ) {
$quote = 'entering' === $state ? ( $depth * 2 ) : ( ( $depth - 1 ) * 2 + 1 );
$buffer .= $syntax['quoting'][ $quote % 4 ];
} elseif ( 'subscripting' === $type ) {
if ( 'entering' === $state && 0 === $depth ) {
$buffer .= '_(';
} elseif ( 'exiting' === $state && 1 === $depth) {
$buffer .= ')';
}
} elseif ( 'superscripting' === $type ) {
if ( 'entering' === $state && 0 === $depth ) {
$buffer .= '^(';
} elseif ( 'exiting' === $state && 1 === $depth) {
$buffer .= ')';
}
} elseif ( ( 0 === $depth && 'entering' === $state ) || ( $depth === 1 && 'exiting' === $state ) ) {
$matching_offset = null;
for ( $k = 0; $k < \count( $indices ); $k++ ) {
if ( $k !== $i && $indices[ $k ] === $index ) {
$matching_offset = $offsets[ $k ];
}
}
if ( ! isset( $matching_offset ) || \abs( $matching_offset ) !== $at ) {
$buffer .= $syntax[ $type ][0];
}
}
$effects[ $type ] += 'entering' === $state ? 1 : -1;
}
if ( $format instanceof WP_Experimental_HTML_Renderer_Format_Link ) {
$url = WP_Experimental_HTML_Renderer_Format_Link::normalize( $format->url, $options->base_url );
/*
* Only render absolute HTTP/S links. Any relative links
* should be expanded here by now. It may be worth rendering
* other special schemes, like `ftp://` and `sftp://` and
* more, but this implementation is currently limited to HTTP.
*/
if (
! \str_starts_with( $url, 'http://' ) &&
! \str_starts_with( $url, 'https://' )
) {
$formats[ $index ] = null;
goto next;
}
if ( 'entering' === $state ) {
$buffer .= "\u{E0001}[";
} else {
$buffer .= "]({$url})\u{E007F}";
}
}
if ( $format instanceof WP_Experimental_HTML_Renderer_Format_Image ) {
/*
* There’s a special case for “block images,” which comprise
* the entirety of the line buffer. In these cases, the image
* should behave more like a block than a format. It might be
* best to handle this elsewhere, but the determination is
* complex. For example, it could be the case that _any_ image
* inside of a PICTURE or FIGURE element should turn into a
* block image, but that information won’t be known here.
*/
if (
1 === \count( $formats ) &&
'' === \trim( $this->buffer, " \r\t\f\n" )
) {
return '' !== $format->title
? ""
: "";
}
if ( '' === $format->alt_text || 'exiting' === $state ) {
$formats[ $index ] = null;
goto next;
}
$buffer .= "⌊{$format->alt_text}⌉";
}
next:
$was_at = $at;
}
if ( $was_at < $length ) {
$buffer .= \substr( $this->buffer, $was_at );
}
return \rtrim( $buffer, " \t\f\r\n" );
}
public function is_empty(): bool {
return ! $this->has_non_whitespace_content();
}
public function has_non_whitespace_content(): bool {
if ( $this->contains_non_whitespace ) {
return true;
}
/*
* If a line buffer comprises only an image, it is a block image
* and therefore not empty in the normal sense.
*/
foreach ( $this->formats as $format ) {
if ( $format instanceof WP_Experimental_HTML_Renderer_Format_Image ) {
return true;
}
}
return false;
}
}