Skip to content

Commit

Permalink
[New Block] Add post time to read block (#43403)
Browse files Browse the repository at this point in the history
* Add new "Post Time To Read" block

* Add fixture

* Update npm dependencies

* Disallow multiple insert

* Fix php lint error

* Fix npm dependencies path

* Remove unused context

* Add a missing word in translate string

* Don't use @wordpress/editor package

* Don't store  attribute

* Fix npm dependencies

* Fix: The block is broken in the post editor

* Update the block icon

* Add prefix and suffix

* Server-side rendering implemented using dummy function

* Update fixtures

* Fix phpcs lint error

* Implement word_count function

* Remove some new lines on php

* Fix regexp escape

* convert float zero to int zero

* Fix PHP lint error

* Cursor isn't displayed in prefix and suffix input area

* Calculate time only when content is changed

* Capitalize when there is no prefix

* Apply inline block style to editor only

* Add Unit Test

* Fix PHP Lint

* Revert prefix/suffix

* Change block title and description

* Fix some unit tests

* Show message when there is no content

* Pass all PHP unit tests

* Add @Covers to Unit Test class

* Changed prefix from gutenberg_ to wp_

* Use dataProvider

* Add render_callback function unit tests

* Add function existence check

* Show time only

* Update PHP unit test

* Mark as experimental

* Move `wp_word_count()` function to `experimental` directory

* Use new function `wp_get_word_count_type()`

* fix lint error

* Change namespace from `formatting` to `l10n`
  • Loading branch information
t-hamano authored Feb 27, 2023
1 parent 7929f82 commit 002be81
Show file tree
Hide file tree
Showing 19 changed files with 677 additions and 1 deletion.
9 changes: 9 additions & 0 deletions docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,15 @@ Post terms. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages
- **Supports:** anchor, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** prefix, separator, suffix, term, textAlign

## Time To Read

Show minutes required to finish reading the post. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/post-time-to-read))

- **Name:** core/post-time-to-read
- **Category:** theme
- **Supports:** ~~html~~, ~~multiple~~
- **Attributes:** textAlign

## Post Title

Displays the title of a post, page, or any other content-type. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/post-title))
Expand Down
1 change: 1 addition & 0 deletions lib/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ function gutenberg_reregister_core_block_types() {
'post-featured-image.php' => 'core/post-featured-image',
'post-navigation-link.php' => 'core/post-navigation-link',
'post-terms.php' => 'core/post-terms',
'post-time-to-read.php' => 'core/post-time-to-read',
'post-title.php' => 'core/post-title',
'query.php' => 'core/query',
'post-template.php' => 'core/post-template',
Expand Down
139 changes: 139 additions & 0 deletions lib/experimental/l10n.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
<?php
/**
* PHP and WordPress configuration compatibility functions for the Gutenberg
* editor plugin changes related to i18n.
*
* @package gutenberg
*/

/**
* Override core's wp_get_word_count_type() introduced in WordPress 6.2.
* Originally, get_word_count_type() method of the WP_Locale class is executed,
* but the process is simulated here.
*
* This function should not be backported to core.
*/
if ( ! function_exists( 'wp_get_word_count_type' ) ) {
/**
* Retrieves the word count type based on the locale.
*
* @return string Locale-specific word count type.
*/
function wp_get_word_count_type() {
$word_count_type = _x( 'words', 'Word count type. Do not translate!', 'gutenberg' );

// Check for valid types.
if ( 'characters_excluding_spaces' !== $word_count_type && 'characters_including_spaces' !== $word_count_type ) {
// Defaults to 'words'.
$word_count_type = 'words';
}
return $word_count_type;
}
}

if ( ! function_exists( 'wp_word_count' ) ) {
/**
* Count words or characters in a provided text string.
*
* @param string $text Text to count elements in.
* @param string $type The type of count. Accepts 'words', 'characters_excluding_spaces', or 'characters_including_spaces'.
* @param array $settings {
* Optional. Array of arguments used to overrides for settings.
*
* @type string $html_regexp Optional. Regular expression to find HTML elements.
* @type string $html_comment_regexp Optional. Regular expression to find HTML comments.
* @type string $space_regexp Optional. Regular expression to find irregular space
* characters.
* @type string $html_entity_regexp Optional. Regular expression to find HTML entities.
* @type string $connector_regexp Optional. Regular expression to find connectors that
* split words.
* @type string $remove_regexp Optional. Regular expression to find remove unwanted
* characters to reduce false-positives.
* @type string $astral_regexp Optional. Regular expression to find unwanted
* characters when searching for non-words.
* @type string $words_regexp Optional. Regular expression to find words by spaces.
* @type string $characters_excluding_spaces_regexp Optional. Regular expression to find characters which
* are non-spaces.
* @type string $characters_including_spaces_regexp Optional. Regular expression to find characters
* including spaces.
* @type array $shortcodes Optional. Array of shortcodes that should be removed
* from the text.
* }
* @return int The word or character count.
*/
function wp_word_count( $text, $type, $settings = array() ) {
$defaults = array(
'html_regexp' => '/<\/?[a-z][^>]*?>/i',
'html_comment_regexp' => '/<!--[\s\S]*?-->/',
'space_regexp' => '/&nbsp;|&#160;/i',
'html_entity_regexp' => '/&\S+?;/',
'connector_regexp' => "/--|\x{2014}/u",
'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",
'astral_regexp' => "/[\x{010000}-\x{10FFFF}]/u",
'words_regexp' => '/\S\s+/u',
'characters_excluding_spaces_regexp' => '/\S/u',
'characters_including_spaces_regexp' => "/[^\f\n\r\t\v\x{00AD}\x{2028}\x{2029}]/u",
'shortcodes' => array(),
);

$count = 0;

if ( ! $text ) {
return $count;
}

$settings = wp_parse_args( $settings, $defaults );

// If there are any shortcodes, add this as a shortcode regular expression.
if ( is_array( $settings['shortcodes'] ) && ! empty( $settings['shortcodes'] ) ) {
$settings['shortcodes_regexp'] = '/\\[\\/?(?:' . implode( '|', $settings['shortcodes'] ) . ')[^\\]]*?\\]/';
}

// Sanitize type to one of three possibilities: 'words', 'characters_excluding_spaces' or 'characters_including_spaces'.
if ( 'characters_excluding_spaces' !== $type && 'characters_including_spaces' !== $type ) {
$type = 'words';
}

$text .= "\n";

// Replace all HTML with a new-line.
$text = preg_replace( $settings['html_regexp'], "\n", $text );

// Remove all HTML comments.
$text = preg_replace( $settings['html_comment_regexp'], '', $text );

// If a shortcode regular expression has been provided use it to remove shortcodes.
if ( ! empty( $settings['shortcodes_regexp'] ) ) {
$text = preg_replace( $settings['shortcodes_regexp'], "\n", $text );
}

// Normalize non-breaking space to a normal space.
$text = preg_replace( $settings['space_regexp'], ' ', $text );

if ( 'words' === $type ) {
// Remove HTML Entities.
$text = preg_replace( $settings['html_entity_regexp'], '', $text );

// Convert connectors to spaces to count attached text as words.
$text = preg_replace( $settings['connector_regexp'], ' ', $text );

// Remove unwanted characters.
$text = preg_replace( $settings['remove_regexp'], '', $text );
} else {
// Convert HTML Entities to "a".
$text = preg_replace( $settings['html_entity_regexp'], 'a', $text );

// Remove surrogate points.
$text = preg_replace( $settings['astral_regexp'], 'a', $text );
}

// Match with the selected type regular expression to count the items.
preg_match_all( $settings[ $type . '_regexp' ], $text, $matches );

if ( $matches ) {
return count( $matches[0] );
}

return $count;
}
}
1 change: 1 addition & 0 deletions lib/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/experimental/blocks.php';
require __DIR__ . '/experimental/navigation-theme-opt-in.php';
require __DIR__ . '/experimental/kses.php';
require __DIR__ . '/experimental/l10n.php';

// Fonts API.
if ( ! class_exists( 'WP_Fonts' ) ) {
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/block-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"@wordpress/server-side-render": "file:../server-side-render",
"@wordpress/url": "file:../url",
"@wordpress/viewport": "file:../viewport",
"@wordpress/wordcount": "file:../wordcount",
"change-case": "^4.1.2",
"classnames": "^2.3.1",
"colord": "^2.7.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/block-library/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import * as postFeaturedImage from './post-featured-image';
import * as postNavigationLink from './post-navigation-link';
import * as postTemplate from './post-template';
import * as postTerms from './post-terms';
import * as postTimeToRead from './post-time-to-read';
import * as postTitle from './post-title';
import * as preformatted from './preformatted';
import * as pullquote from './pullquote';
Expand Down Expand Up @@ -197,6 +198,7 @@ const getAllBlocks = () =>
postTerms,
postNavigationLink,
postTemplate,
postTimeToRead,
queryPagination,
queryPaginationNext,
queryPaginationNumbers,
Expand Down
20 changes: 20 additions & 0 deletions packages/block-library/src/post-time-to-read/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 2,
"__experimental": true,
"name": "core/post-time-to-read",
"title": "Time To Read",
"category": "theme",
"description": "Show minutes required to finish reading the post.",
"textdomain": "default",
"usesContext": [ "postId", "postType" ],
"attributes": {
"textAlign": {
"type": "string"
}
},
"supports": {
"html": false,
"multiple": false
}
}
101 changes: 101 additions & 0 deletions packages/block-library/src/post-time-to-read/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* External dependencies
*/
import classnames from 'classnames';

/**
* WordPress dependencies
*/
import { _x, _n, sprintf } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
import {
AlignmentControl,
BlockControls,
useBlockProps,
} from '@wordpress/block-editor';
import { __unstableSerializeAndClean } from '@wordpress/blocks';
import { useEntityProp, useEntityBlockEditor } from '@wordpress/core-data';
import { count as wordCount } from '@wordpress/wordcount';

/**
* Average reading rate - based on average taken from
* https://irisreading.com/average-reading-speed-in-various-languages/
* (Characters/minute used for Chinese rather than words).
*/
const AVERAGE_READING_RATE = 189;

function PostTimeToReadEdit( { attributes, setAttributes, context } ) {
const { textAlign } = attributes;
const { postId, postType } = context;

const [ contentStructure ] = useEntityProp(
'postType',
postType,
'content',
postId
);

const [ blocks ] = useEntityBlockEditor( 'postType', postType, {
id: postId,
} );

const minutesToReadString = useMemo( () => {
// Replicates the logic found in getEditedPostContent().
let content;
if ( contentStructure instanceof Function ) {
content = contentStructure( { blocks } );
} else if ( blocks ) {
// If we have parsed blocks already, they should be our source of truth.
// Parsing applies block deprecations and legacy block conversions that
// unparsed content will not have.
content = __unstableSerializeAndClean( blocks );
} else {
content = contentStructure;
}

/*
* translators: If your word count is based on single characters (e.g. East Asian characters),
* enter 'characters_excluding_spaces' or 'characters_including_spaces'. Otherwise, enter 'words'.
* Do not translate into your own language.
*/
const wordCountType = _x(
'words',
'Word count type. Do not translate!'
);

const minutesToRead = Math.max(
1,
Math.round(
wordCount( content, wordCountType ) / AVERAGE_READING_RATE
)
);

return sprintf(
/* translators: %d is the number of minutes the post will take to read. */
_n( '%d minute', '%d minutes', minutesToRead ),
minutesToRead
);
}, [ contentStructure, blocks ] );

const blockProps = useBlockProps( {
className: classnames( {
[ `has-text-align-${ textAlign }` ]: textAlign,
} ),
} );

return (
<>
<BlockControls group="block">
<AlignmentControl
value={ textAlign }
onChange={ ( nextAlign ) => {
setAttributes( { textAlign: nextAlign } );
} }
/>
</BlockControls>
<p { ...blockProps }>{ minutesToReadString }</p>
</>
);
}

export default PostTimeToReadEdit;
15 changes: 15 additions & 0 deletions packages/block-library/src/post-time-to-read/icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* WordPress dependencies
*/
import { SVG, Path } from '@wordpress/components';

export default (
<SVG
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<Path d="M12 3c-5 0-9 4-9 9s4 9 9 9 9-4 9-9-4-9-9-9zm0 16.5c-4.1 0-7.5-3.4-7.5-7.5S7.9 4.5 12 4.5s7.5 3.4 7.5 7.5-3.4 7.5-7.5 7.5zM12 7l-1 5c0 .3.2.6.4.8l4.2 2.8-2.7-4.1L12 7z" />
</SVG>
);
17 changes: 17 additions & 0 deletions packages/block-library/src/post-time-to-read/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Internal dependencies
*/
import initBlock from '../utils/init-block';
import metadata from './block.json';
import edit from './edit';
import icon from './icon';

const { name } = metadata;
export { metadata, name };

export const settings = {
icon,
edit,
};

export const init = () => initBlock( { name, metadata, settings } );
Loading

1 comment on commit 002be81

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flaky tests detected in 002be81.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/4280625888
📝 Reported issues:

Please sign in to comment.