| 1 |
<?php |
| 2 |
/** |
| 3 |
* Server-side rendering of the `core/post-time-to-read` block. |
| 4 |
* |
| 5 |
* @package WordPress |
| 6 |
*/ |
| 7 |
|
| 8 |
/** |
| 9 |
* Counts words or characters in a provided text string. |
| 10 |
* |
| 11 |
* This function currently employs an array of regular expressions |
| 12 |
* to parse HTML and count words, which may result in inaccurate |
| 13 |
* word counts. However, it is designed primarily to agree with the |
| 14 |
* corresponding JavaScript function. |
| 15 |
* |
| 16 |
* Any improvements in the word counting, for example with the HTML API |
| 17 |
* and {@see \IntlBreakIterator::createWordInstance()} should coordinate |
| 18 |
* with changes to the JavaScript implementation to ensure consistency |
| 19 |
* between the editor and the rendered page. |
| 20 |
* |
| 21 |
* @since 6.9.0 |
| 22 |
* |
| 23 |
* @param string $text Text to count elements in. |
| 24 |
* @param string $type The type of count. Accepts 'words', 'characters_excluding_spaces', or 'characters_including_spaces'. |
| 25 |
* |
| 26 |
* @return string The rendered word count. |
| 27 |
*/ |
| 28 |
function gutenberg_block_core_post_time_to_read_word_count( $text, $type ) { |
| 29 |
$settings = array( |
| 30 |
'html_regexp' => '/<\/?[a-z][^>]*?>/i', |
| 31 |
'html_comment_regexp' => '/<!--[\s\S]*?-->/', |
| 32 |
'space_regexp' => '/ | /i', |
| 33 |
'html_entity_regexp' => '/&\S+?;/', |
| 34 |
'connector_regexp' => "/--|\x{2014}/u", |
| 35 |
'remove_regexp' => "/[\x{0021}-\x{0040}\x{005B}-\x{0060}\x{007B}-\x{007E}\x{0080}-\x{00BF}\x{00D7}\x{00F7}\x{2000}-\x{2BFF}\x{2E00}-\x{2E7F}]/u", |
| 36 |
'astral_regexp' => "/[\x{010000}-\x{10FFFF}]/u", |
| 37 |
'words_regexp' => '/\S\s+/u', |
| 38 |
'characters_excluding_spaces_regexp' => '/\S/u', |
| 39 |
'characters_including_spaces_regexp' => "/[^\f\n\r\t\v\x{00AD}\x{2028}\x{2029}]/u", |
| 40 |
); |
| 41 |
|
| 42 |
$count = 0; |
| 43 |
|
| 44 |
if ( '' === trim( $text ) ) { |
| 45 |
return $count; |
| 46 |
} |
| 47 |
|
| 48 |
// Sanitize type to one of three possibilities: 'words', 'characters_excluding_spaces' or 'characters_including_spaces'. |
| 49 |
if ( 'characters_excluding_spaces' !== $type && 'characters_including_spaces' !== $type ) { |
| 50 |
$type = 'words'; |
| 51 |
} |
| 52 |
|
| 53 |
$text .= "\n"; |
| 54 |
|
| 55 |
// Replace all HTML with a new-line. |
| 56 |
$text = preg_replace( $settings['html_regexp'], "\n", $text ); |
| 57 |
|
| 58 |
// Remove all HTML comments. |
| 59 |
$text = preg_replace( $settings['html_comment_regexp'], '', $text ); |
| 60 |
|
| 61 |
// If a shortcode regular expression has been provided use it to remove shortcodes. |
| 62 |
if ( ! empty( $settings['shortcodes_regexp'] ) ) { |
| 63 |
$text = preg_replace( $settings['shortcodes_regexp'], "\n", $text ); |
| 64 |
} |
| 65 |
|
| 66 |
// Normalize non-breaking space to a normal space. |
| 67 |
$text = preg_replace( $settings['space_regexp'], ' ', $text ); |
| 68 |
|
| 69 |
if ( 'words' === $type ) { |
| 70 |
// Remove HTML Entities. |
| 71 |
$text = preg_replace( $settings['html_entity_regexp'], '', $text ); |
| 72 |
|
| 73 |
// Convert connectors to spaces to count attached text as words. |
| 74 |
$text = preg_replace( $settings['connector_regexp'], ' ', $text ); |
| 75 |
|
| 76 |
// Remove unwanted characters. |
| 77 |
$text = preg_replace( $settings['remove_regexp'], '', $text ); |
| 78 |
} else { |
| 79 |
// Convert HTML Entities to "a". |
| 80 |
$text = preg_replace( $settings['html_entity_regexp'], 'a', $text ); |
| 81 |
|
| 82 |
// Remove surrogate points. |
| 83 |
$text = preg_replace( $settings['astral_regexp'], 'a', $text ); |
| 84 |
} |
| 85 |
|
| 86 |
// Match with the selected type regular expression to count the items. |
| 87 |
return (int) preg_match_all( $settings[ $type . '_regexp' ], $text ); |
| 88 |
} |
| 89 |
|
| 90 |
/** |
| 91 |
* Renders the `core/post-time-to-read` block on the server. |
| 92 |
* |
| 93 |
* @since 6.9.0 |
| 94 |
* |
| 95 |
* @param array $attributes Block attributes. |
| 96 |
* @param string $content Block default content. |
| 97 |
* @param WP_Block $block Block instance. |
| 98 |
* @return string Returns the rendered post author name block. |
| 99 |
*/ |
| 100 |
function gutenberg_render_block_core_post_time_to_read( $attributes, $content, $block ) { |
| 101 |
if ( ! isset( $block->context['postId'] ) ) { |
| 102 |
return ''; |
| 103 |
} |
| 104 |
|
| 105 |
$content = get_the_content(); |
| 106 |
$average_reading_rate = $attributes['averageReadingSpeed'] ?? 189; |
| 107 |
|
| 108 |
$display_mode = $attributes['displayMode'] ?? 'time'; |
| 109 |
|
| 110 |
$word_count_type = wp_get_word_count_type(); |
| 111 |
$total_words = gutenberg_block_core_post_time_to_read_word_count( $content, $word_count_type ); |
| 112 |
|
| 113 |
$parts = array(); |
| 114 |
|
| 115 |
// Add "time to read" part, if enabled. |
| 116 |
if ( 'time' === $display_mode ) { |
| 117 |
if ( ! empty( $attributes['displayAsRange'] ) ) { |
| 118 |
// Calculate faster reading rate with 20% speed = lower minutes, |
| 119 |
// and slower reading rate with 20% speed = higher minutes. |
| 120 |
$min_minutes = max( 1, (int) round( $total_words / $average_reading_rate * 0.8 ) ); |
| 121 |
$max_minutes = max( 1, (int) round( $total_words / $average_reading_rate * 1.2 ) ); |
| 122 |
if ( $min_minutes === $max_minutes ) { |
| 123 |
$max_minutes = $min_minutes + 1; |
| 124 |
} |
| 125 |
/* translators: 1: minimum minutes, 2: maximum minutes to read the post. */ |
| 126 |
$time_string = sprintf( |
| 127 |
/* translators: 1: minimum minutes, 2: maximum minutes to read the post. */ |
| 128 |
_x( '%1$s–%2$s minutes', 'Range of minutes to read' ), |
| 129 |
$min_minutes, |
| 130 |
$max_minutes |
| 131 |
); |
| 132 |
} else { |
| 133 |
$minutes_to_read = max( 1, (int) round( $total_words / $average_reading_rate ) ); |
| 134 |
$time_string = sprintf( |
| 135 |
/* translators: %s: the number of minutes to read the post. */ |
| 136 |
_n( '%s minute', '%s minutes', $minutes_to_read ), |
| 137 |
$minutes_to_read |
| 138 |
); |
| 139 |
} |
| 140 |
$parts[] = $time_string; |
| 141 |
} |
| 142 |
|
| 143 |
// Add "word count" part, if enabled. |
| 144 |
if ( 'words' === $display_mode ) { |
| 145 |
$word_count_string = 'words' === $word_count_type ? sprintf( |
| 146 |
/* translators: %s: the number of words in the post. */ |
| 147 |
_n( '%s word', '%s words', $total_words ), |
| 148 |
number_format_i18n( $total_words ) |
| 149 |
) : sprintf( |
| 150 |
/* translators: %s: the number of characters in the post. */ |
| 151 |
_n( '%s character', '%s characters', $total_words ), |
| 152 |
number_format_i18n( $total_words ) |
| 153 |
); |
| 154 |
$parts[] = $word_count_string; |
| 155 |
} |
| 156 |
|
| 157 |
$display_string = implode( '<br>', $parts ); |
| 158 |
|
| 159 |
$align_class_name = empty( $attributes['textAlign'] ) ? '' : "has-text-align-{$attributes['textAlign']}"; |
| 160 |
|
| 161 |
$wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $align_class_name ) ); |
| 162 |
|
| 163 |
return sprintf( |
| 164 |
'<div %1$s>%2$s</div>', |
| 165 |
$wrapper_attributes, |
| 166 |
$display_string |
| 167 |
); |
| 168 |
} |
| 169 |
|
| 170 |
|
| 171 |
/** |
| 172 |
* Registers the `core/post-time-to-read` block on the server. |
| 173 |
* |
| 174 |
* @since 6.9.0 |
| 175 |
*/ |
| 176 |
function gutenberg_register_block_core_post_time_to_read() { |
| 177 |
register_block_type_from_metadata( |
| 178 |
__DIR__ . '/post-time-to-read', |
| 179 |
array( |
| 180 |
'render_callback' => 'gutenberg_render_block_core_post_time_to_read', |
| 181 |
) |
| 182 |
); |
| 183 |
} |
| 184 |
|
| 185 |
add_action( 'init', 'gutenberg_register_block_core_post_time_to_read', 20 ); |
| 186 |
|