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
betterdocs / includes / Editors / BlockEditor / Blocks / ReadingTime.php

ReadingTime.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.4, at includes/Editors/BlockEditor/Blocks/ReadingTime.php

263 lines 9.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Editors\BlockEditor\Blocks;
4
5 use WPDeveloper\BetterDocs\Editors\BlockEditor\Block;
6 use WPDeveloper\BetterDocs\Shortcodes\AIActions;
7 use WPDeveloper\BetterDocs\Shortcodes\Listen;
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 */
24 class ReadingTime extends Block {
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
36 protected $frontend_styles = [
37 'reading-time',
38 'betterdocs-ai-actions',
39 'betterdocs-listen'
40 ];
41
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'
46 ];
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
68 public function get_name() {
69 return 'reading-time';
70 }
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 */
82 public function get_default_attributes() {
83 $settings = betterdocs()->settings;
84
85 $defaults = [
86 'blockId' => '',
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' )
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;
104 }
105
106 public function render( $attributes, $content ) {
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>';
141 }
142
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'] : ''
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;
261 }
262 }
263