PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 All 202 releases
← All changes | includes/Editors/BlockEditor/Blocks/ReadingTime.php +234 -14 4.7.0 → 4.9.4 View file →
@@ -2,41 +2,261 @@
2 2
3 3 namespace WPDeveloper\BetterDocs\Editors\BlockEditor\Blocks;
4 4
5 5 use WPDeveloper\BetterDocs\Editors\BlockEditor\Block;
6 +use WPDeveloper\BetterDocs\Shortcodes\AIActions;
7 +use WPDeveloper\BetterDocs\Shortcodes\Listen;
6 8
9 +/**
10 + * betterdocs/reading-time — the single-doc meta row.
11 + *
12 + * Three independent parts: estimated reading time and the Listen pill on the
13 + * leading edge, the AI Actions "Copy page" button on the trailing edge, each with
14 + * its own switch. The block name stays `reading-time` because it is what every
15 + * saved post, pattern and FSE template already refers to; only the title
16 + * changed.
17 + *
18 + * Every control defaults to the matching Settings-panel value, so an untouched
19 + * block renders exactly what the site-wide configuration says and the controls
20 + * exist to override one instance. See the JS counterpart in
21 + * `react-src/gutenberg/blocks/reading-time/src/settingDefaults.js` — the two
22 + * halves have to agree or the editor preview and the frontend diverge.
23 + */
7 24 class ReadingTime extends Block {
8 25
26 + // Appended, not replaced: Block::$editor_styles defaults to the BetterDocs
27 + // editor stylesheet, and assigning over it would take that away from this
28 + // block alone.
29 + protected $editor_styles = [
30 + 'betterdocs-blocks-editor',
31 + 'reading-time',
32 + 'betterdocs-ai-actions',
33 + 'betterdocs-listen'
34 + ];
35 +
9 36 protected $frontend_styles = [
10 - 'reading-time'
37 + 'reading-time',
38 + 'betterdocs-ai-actions',
39 + 'betterdocs-listen'
11 40 ];
12 41
13 - protected $editor_styles = [
14 - 'reading-time'
42 + // Two of the three parts are interactive: clipboard copy and the disclosure menu
43 + // for AI Actions, the whole player for Listen.
44 + protected $frontend_scripts = [
45 + 'betterdocs'
15 46 ];
16 47
48 + /**
49 + * Action id => [ block attribute, Settings key ].
50 + *
51 + * Ids and Settings keys come from Core\AIActions::registry(); the attribute
52 + * names come from `src/inspector.js`, which lists them in the same order.
53 + * Spelled out rather than derived because the three of these that a rule would
54 + * get right are outnumbered by the four it would not.
55 + *
56 + * @var array<string, string[]>
57 + */
58 + const AI_ACTIONS = [
59 + 'copy-page' => [ 'aiCopyPage', 'ai_actions_copy_page' ],
60 + 'view-markdown' => [ 'aiViewMarkdown', 'ai_actions_view_markdown' ],
61 + 'open-chatgpt' => [ 'aiChatGPT', 'ai_actions_chatgpt' ],
62 + 'open-claude' => [ 'aiClaude', 'ai_actions_claude' ],
63 + 'open-aistudio' => [ 'aiGemini', 'ai_actions_gemini' ],
64 + 'open-perplexity' => [ 'aiPerplexity', 'ai_actions_perplexity' ],
65 + 'open-grok' => [ 'aiGrok', 'ai_actions_grok' ]
66 + ];
67 +
17 68 public function get_name() {
18 69 return 'reading-time';
19 70 }
20 71
72 + /**
73 + * Server-side fallbacks for attributes an older saved block does not carry,
74 + * and for any control Gutenberg omitted because it still equals its default.
75 + *
76 + * These read the Settings panel for the same reason the JS defaults do: a
77 + * block placed before this feature existed has none of the new attributes, and
78 + * should behave as though its controls were never touched.
79 + *
80 + * @return array
81 + */
21 82 public function get_default_attributes() {
22 - return [
83 + $settings = betterdocs()->settings;
84 +
85 + $defaults = [
23 86 'blockId' => '',
24 - 'readingTimeTitle' => '',
25 - 'readingTimeText' => __( 'min read', 'betterdocs' ),
26 - 'singularReadingTimeText' => __( 'min read', 'betterdocs' )
87 + 'enableReadingTime' => (bool) $settings->get( 'enable_estimated_reading_time' ),
88 + 'readingTimeTitle' => (string) $settings->get( 'estimated_reading_time_title' ),
89 + 'readingTimeText' => (string) $settings->get( 'estimated_reading_time_text' ),
90 + 'singularReadingTimeText' => (string) $settings->get( 'singular_estimated_reading_time_text' ),
91 + 'enableAIActions' => (bool) $settings->get( 'enable_ai_actions' ),
92 + 'aiButtonLabel' => (string) $settings->get( 'ai_actions_button_label' ),
93 + 'aiPromptTemplate' => (string) $settings->get( 'ai_actions_prompt_template' ),
94 + 'enableListen' => (bool) $settings->get( 'enable_listen' ),
95 + 'listenButtonLabel' => (string) $settings->get( 'listen_button_label' ),
96 + 'listenShowSpeed' => (bool) $settings->get( 'listen_show_speed' )
27 97 ];
98 +
99 + foreach ( self::AI_ACTIONS as list( $attribute, $setting_key ) ) {
100 + $defaults[ $attribute ] = (bool) $settings->get( $setting_key );
101 + }
102 +
103 + return $defaults;
28 104 }
29 105
30 106 public function render( $attributes, $content ) {
31 - $settings = $attributes;
32 - echo '<div class="' . esc_attr( $settings['blockId'] ) . '">';
33 - echo do_shortcode( '[betterdocs_reading_time singular_reading_text="' . esc_html( $settings['singularReadingTimeText'] ) . '" reading_text="' . esc_html( $settings['readingTimeText'] ) . '" reading_title="' . esc_html( $settings['readingTimeTitle'] ) . '"]' );
34 - echo '</div>';
107 + $reading_time = $this->reading_time_markup( $attributes );
108 + $listen = $this->listen_markup( $attributes );
109 + $ai_actions = $this->ai_actions_markup( $attributes );
110 +
111 + // Nothing switched on, or a doc where nothing resolves: emit no wrapper
112 + // rather than an empty flex row taking up vertical space. Matches what
113 + // views/templates/parts/doc-meta.php does for the classic layouts.
114 + if ( '' === $reading_time && '' === $listen && '' === $ai_actions ) {
115 + return;
116 + }
117 +
118 + // The same wrapper views/templates/parts/doc-meta.php emits, so the block
119 + // inherits the classic row's layout — including the child margin reset that
120 + // keeps the button on the reading-time pill's centre line — without a line
121 + // of CSS of its own.
122 + printf(
123 + '<div class="%s"><div class="betterdocs-doc-meta">',
124 + esc_attr( isset( $attributes['blockId'] ) ? $attributes['blockId'] : '' )
125 + );
126 +
127 + // One leading box holding both, rather than one each: the row's `> *` rule
128 + // zeroes child block margins, and the 8px gap between the pills is the
129 + // leading box's own, so two boxes would space them by the row's wider 16px.
130 + $leading = $reading_time . $listen;
131 +
132 + if ( '' !== $leading ) {
133 + echo '<div class="betterdocs-doc-meta-start">' . $leading . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered template parts, escaped at source.
134 + }
135 +
136 + if ( '' !== $ai_actions ) {
137 + echo '<div class="betterdocs-doc-meta-end">' . $ai_actions . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- rendered template parts, escaped at source.
138 + }
139 +
140 + echo '</div></div>';
35 141 }
36 142
37 - public function view_params() {
38 - return [
39 - 'reactions_text' => $this->attributes['reaction_text']
143 + /**
144 + * @param array $attributes
145 + * @return string
146 + */
147 + protected function reading_time_markup( $attributes ) {
148 + if ( empty( $attributes['enableReadingTime'] ) ) {
149 + return '';
150 + }
151 +
152 + // esc_attr, not esc_html: these land inside double-quoted shortcode
153 + // attributes, where an unescaped quote would break out of the attribute.
154 + return (string) do_shortcode(
155 + '[betterdocs_reading_time'
156 + . ' singular_reading_text="' . esc_attr( $attributes['singularReadingTimeText'] ) . '"'
157 + . ' reading_text="' . esc_attr( $attributes['readingTimeText'] ) . '"'
158 + . ' reading_title="' . esc_attr( $attributes['readingTimeTitle'] ) . '"]'
159 + );
160 + }
161 +
162 + /**
163 + * @param array $attributes
164 + * @return string
165 + */
166 + protected function listen_markup( $attributes ) {
167 + if ( empty( $attributes['enableListen'] ) || ! isset( betterdocs()->listen ) ) {
168 + return '';
169 + }
170 +
171 + // Handed off by token rather than as attributes, for the reason
172 + // Shortcodes\Listen::push_context() gives: `show_speed` is tri-state, and a
173 + // shortcode attribute string cannot express "inherit the setting".
174 + $context = Listen::push_context( $this->listen_args( $attributes ) );
175 +
176 + return trim(
177 + (string) do_shortcode( '[betterdocs_listen context="' . esc_attr( $context ) . '"]' )
178 + );
179 + }
180 +
181 + /**
182 + * Per-instance overrides for Core\Listen::render().
183 + *
184 + * A blank label means "inherit", so it is left out entirely rather than passed
185 + * as an empty string. `show_speed` is passed whenever the attribute exists at
186 + * all, because false is a legitimate value there and omitting it would fall
187 + * back to the site-wide setting instead of switching the control off.
188 + *
189 + * @param array $attributes
190 + * @return array
191 + */
192 + protected function listen_args( $attributes ) {
193 + $args = [
194 + 'widget_type' => 'blocks',
195 + 'blockId' => isset( $attributes['blockId'] ) ? $attributes['blockId'] : ''
40 196 ];
197 +
198 + if ( ! empty( $attributes['listenButtonLabel'] ) ) {
199 + $args['button_label'] = (string) $attributes['listenButtonLabel'];
200 + }
201 +
202 + if ( isset( $attributes['listenShowSpeed'] ) ) {
203 + $args['show_speed'] = (bool) $attributes['listenShowSpeed'];
204 + }
205 +
206 + return $args;
207 + }
208 +
209 + /**
210 + * @param array $attributes
211 + * @return string
212 + */
213 + protected function ai_actions_markup( $attributes ) {
214 + if ( empty( $attributes['enableAIActions'] ) || ! isset( betterdocs()->ai_actions ) ) {
215 + return '';
216 + }
217 +
218 + // Handed off by token rather than as attributes: the args carry a tri-state
219 + // `enabled_actions` map and free text that a shortcode attribute string
220 + // cannot round-trip. See AIActions::push_context().
221 + $context = AIActions::push_context( $this->ai_actions_args( $attributes ) );
222 +
223 + return trim(
224 + (string) do_shortcode( '[betterdocs_ai_actions context="' . esc_attr( $context ) . '"]' )
225 + );
226 + }
227 +
228 + /**
229 + * Per-instance overrides for Core\AIActions::render().
230 + *
231 + * A blank text control means "inherit", so it is left out entirely rather than
232 + * passed as an empty string — render() treats a blank override as absent, but
233 + * omitting it keeps the intent legible at the call site.
234 + *
235 + * @param array $attributes
236 + * @return array
237 + */
238 + protected function ai_actions_args( $attributes ) {
239 + $enabled = [];
240 + foreach ( self::AI_ACTIONS as $id => list( $attribute ) ) {
241 + if ( isset( $attributes[ $attribute ] ) ) {
242 + $enabled[ $id ] = (bool) $attributes[ $attribute ];
243 + }
244 + }
245 +
246 + $args = [
247 + 'widget_type' => 'blocks',
248 + 'blockId' => isset( $attributes['blockId'] ) ? $attributes['blockId'] : '',
249 + 'enabled_actions' => $enabled
250 + ];
251 +
252 + if ( ! empty( $attributes['aiButtonLabel'] ) ) {
253 + $args['button_label'] = (string) $attributes['aiButtonLabel'];
254 + }
255 +
256 + if ( ! empty( $attributes['aiPromptTemplate'] ) ) {
257 + $args['prompt_template'] = (string) $attributes['aiPromptTemplate'];
258 + }
259 +
260 + return $args;
41 261 }
42 262 }