diff --git a/package-lock.json b/package-lock.json
index 1fc9131a33c8c3..20dc65f7ed872c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34395,6 +34395,16 @@
"signal-exit": "^3.0.2"
}
},
+ "node_modules/locutus": {
+ "version": "2.0.32",
+ "resolved": "https://registry.npmjs.org/locutus/-/locutus-2.0.32.tgz",
+ "integrity": "sha512-fr7OCpbE4xeefhHqfh6hM2/l9ZB3XvClHgtgFnQNImrM/nqL950o6FO98vmUH8GysfQRCcyBYtZ4C8GcY52Edw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10",
+ "yarn": ">= 1"
+ }
+ },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -54764,7 +54774,8 @@
"@wordpress/icons": "*",
"@wordpress/private-apis": "*",
"@wordpress/rich-text": "*",
- "@wordpress/url": "*"
+ "@wordpress/url": "*",
+ "locutus": "2.0.32"
},
"engines": {
"node": ">=18.12.0",
diff --git a/packages/format-library/package.json b/packages/format-library/package.json
index ee1dd8efd1fe95..d23987579b6081 100644
--- a/packages/format-library/package.json
+++ b/packages/format-library/package.json
@@ -39,7 +39,8 @@
"@wordpress/icons": "*",
"@wordpress/private-apis": "*",
"@wordpress/rich-text": "*",
- "@wordpress/url": "*"
+ "@wordpress/url": "*",
+ "locutus": "2.0.32"
},
"peerDependencies": {
"react": "^18.0.0",
diff --git a/packages/format-library/src/default-formats.js b/packages/format-library/src/default-formats.js
index 3ccfc92cb40c18..0c9bfa4c9adece 100644
--- a/packages/format-library/src/default-formats.js
+++ b/packages/format-library/src/default-formats.js
@@ -9,6 +9,7 @@ import { link } from './link';
import { strikethrough } from './strikethrough';
import { underline } from './underline';
import { textColor } from './text-color';
+import { time } from './time';
import { subscript } from './subscript';
import { superscript } from './superscript';
import { keyboard } from './keyboard';
@@ -25,6 +26,7 @@ export default [
strikethrough,
underline,
textColor,
+ time,
subscript,
superscript,
keyboard,
diff --git a/packages/format-library/src/time/index.js b/packages/format-library/src/time/index.js
new file mode 100644
index 00000000000000..8bf51d54ccf4a9
--- /dev/null
+++ b/packages/format-library/src/time/index.js
@@ -0,0 +1,98 @@
+/**
+ * External dependencies
+ */
+import { gmdate, strtotime } from 'locutus/php/datetime';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+import { toggleFormat } from '@wordpress/rich-text';
+import {
+ RichTextToolbarButton,
+ RichTextShortcut,
+} from '@wordpress/block-editor';
+import { postDate as icon } from '@wordpress/icons';
+
+const name = 'core/time';
+const title = __( 'Time' );
+
+export const time = {
+ name,
+ title,
+ tagName: 'time',
+ className: null,
+ attributes: {
+ datetime: 'datetime',
+ },
+ edit( { isActive, value, onChange, onFocus } ) {
+ function onClick() {
+ const dateDescription = value.text
+ .slice( value.start, value.end )
+ .trim();
+
+ // Exit early if no selection or format is already active
+ if ( ! dateDescription || isActive ) {
+ onChange(
+ toggleFormat( value, {
+ type: name,
+ } )
+ );
+ return;
+ }
+
+ // Clean up the date string
+ const dateCleaned = dateDescription
+ .replace( 'at ', '' ) // Remove "at"
+ .replace( 'UTC', 'GMT' ); // Replace "UTC" with "GMT"
+
+ // Parse the date string using strtotime
+ const timestamp = strtotime( dateCleaned );
+
+ // If parsing fails, toggle format without enhancement
+ if ( ! timestamp ) {
+ onChange(
+ toggleFormat( value, {
+ type: name,
+ } )
+ );
+ return;
+ }
+
+ // Format the datetime attributes using gmdate
+ const datetime = gmdate( 'c', timestamp ); // ISO 8601 format
+ const datetimeISO = gmdate( 'Ymd\\THi', timestamp ); // Compact ISO format
+
+ // Apply the format with parsed datetime attributes
+ onChange(
+ toggleFormat( value, {
+ type: name,
+ attributes: {
+ datetime,
+ 'data-iso': datetimeISO,
+ style: 'text-decoration: underline; text-decoration-style: dotted',
+ },
+ } )
+ );
+
+ onFocus();
+ }
+
+ return (
+ <>
+
+
+ >
+ );
+ },
+};