| 1 |
<?php |
| 2 |
|
| 3 |
namespace WPDeveloper\BetterDocs\Shortcodes; |
| 4 |
|
| 5 |
use WPDeveloper\BetterDocs\Core\Shortcode; |
| 6 |
|
| 7 |
/** |
| 8 |
* [betterdocs_ai_actions] — the "Copy page" split button. |
| 9 |
* |
| 10 |
* The single render path for the feature, the way [betterdocs_reading_time] is |
| 11 |
* for reading time: the classic single-doc templates, the Gutenberg block and |
| 12 |
* the Elementor widget all reach the button through this shortcode rather than |
| 13 |
* calling Core\AIActions::render() themselves. |
| 14 |
* |
| 15 |
* Attributes: |
| 16 |
* enable bool Render at all. Default true. |
| 17 |
* post_id int Which doc to act on. Defaults to the current post, |
| 18 |
* which is what you want inside a doc template and |
| 19 |
* useless outside one. |
| 20 |
* actions string Comma-separated allow-list of action ids, e.g. |
| 21 |
* "copy-page,open-chatgpt". Listed actions are forced |
| 22 |
* on and every other action off. Omit to inherit the |
| 23 |
* global per-action settings. |
| 24 |
* button_label string Overrides the `ai_actions_button_label` setting. |
| 25 |
* prompt_template string Overrides the `ai_actions_prompt_template` setting. |
| 26 |
* Cannot contain `]` — WP's shortcode regex excludes it |
| 27 |
* from the attribute run, so the tag is truncated there |
| 28 |
* and the remainder spills into the page as body text. |
| 29 |
* Set the `ai_actions_prompt_template` setting instead |
| 30 |
* when the prompt needs a bracket. |
| 31 |
* context string Internal. See push_context(). |
| 32 |
* |
| 33 |
* @since 4.8.0 |
| 34 |
*/ |
| 35 |
class AIActions extends Shortcode { |
| 36 |
/** |
| 37 |
* One-shot argument handoffs for the block and the widget. |
| 38 |
* |
| 39 |
* Those two callers hold a typed args array — an `enabled_actions` map plus |
| 40 |
* free-text overrides — and none of it survives a round trip through a |
| 41 |
* shortcode attribute string: |
| 42 |
* |
| 43 |
* - WP's shortcode regex excludes `]` from the attribute run, and esc_attr() |
| 44 |
* does not escape it, so a `]` anywhere in `prompt_template` (long, user |
| 45 |
* editable, and the place a bracket is most likely to appear) truncates the |
| 46 |
* tag and dumps the rest as body text. |
| 47 |
* - `enabled_actions` is tri-state — absent means "inherit the global |
| 48 |
* setting", present means "force on/off" (Core\AIActions::action_enabled()) |
| 49 |
* — and a flat comma list can only express two of those three. |
| 50 |
* |
| 51 |
* So internal callers stash the array here, pass the opaque token, and the |
| 52 |
* shortcode pops it. Tokens are consumed on read, so a value cannot leak into |
| 53 |
* a second instance on the same page. |
| 54 |
* |
| 55 |
* @var array<string, array> |
| 56 |
*/ |
| 57 |
private static $contexts = []; |
| 58 |
|
| 59 |
/** |
| 60 |
* Stash a typed args array and get back a shortcode-safe token. |
| 61 |
* |
| 62 |
* @param array $args As accepted by Core\AIActions::render(). |
| 63 |
* @return string |
| 64 |
*/ |
| 65 |
public static function push_context( array $args ) { |
| 66 |
$token = wp_unique_id( 'bd-ai-' ); |
| 67 |
self::$contexts[ $token ] = $args; |
| 68 |
|
| 69 |
return $token; |
| 70 |
} |
| 71 |
|
| 72 |
/** |
| 73 |
* Pop a stashed args array. Returns null when the token is unknown, so the |
| 74 |
* caller can tell "no context" from "context that happened to be empty". |
| 75 |
* |
| 76 |
* @param string $token |
| 77 |
* @return array|null |
| 78 |
*/ |
| 79 |
private static function pull_context( $token ) { |
| 80 |
if ( ! isset( self::$contexts[ $token ] ) ) { |
| 81 |
return null; |
| 82 |
} |
| 83 |
|
| 84 |
$args = self::$contexts[ $token ]; |
| 85 |
unset( self::$contexts[ $token ] ); |
| 86 |
|
| 87 |
return $args; |
| 88 |
} |
| 89 |
|
| 90 |
public function get_name() { |
| 91 |
return 'betterdocs_ai_actions'; |
| 92 |
} |
| 93 |
|
| 94 |
public function get_style_depends() { |
| 95 |
return [ 'betterdocs-ai-actions' ]; |
| 96 |
} |
| 97 |
|
| 98 |
public function get_script_depends() { |
| 99 |
return [ 'betterdocs' ]; |
| 100 |
} |
| 101 |
|
| 102 |
public function default_attributes() { |
| 103 |
return [ |
| 104 |
'enable' => true, |
| 105 |
'post_id' => 0, |
| 106 |
'actions' => '', |
| 107 |
'button_label' => '', |
| 108 |
'prompt_template' => '', |
| 109 |
'context' => '' |
| 110 |
]; |
| 111 |
} |
| 112 |
|
| 113 |
public function render( $atts, $content = null ) { |
| 114 |
if ( ! $this->isset( 'enable' ) ) { |
| 115 |
return; |
| 116 |
} |
| 117 |
|
| 118 |
$post_id = ! empty( $this->attributes['post_id'] ) ? (int) $this->attributes['post_id'] : get_the_ID(); |
| 119 |
|
| 120 |
if ( ! $post_id ) { |
| 121 |
return; |
| 122 |
} |
| 123 |
|
| 124 |
betterdocs()->ai_actions->render( $post_id, $this->render_args() ); |
| 125 |
} |
| 126 |
|
| 127 |
/** |
| 128 |
* Per-instance overrides for Core\AIActions::render(). |
| 129 |
* |
| 130 |
* @return array |
| 131 |
*/ |
| 132 |
protected function render_args() { |
| 133 |
$context = '' !== (string) $this->attributes['context'] |
| 134 |
? self::pull_context( (string) $this->attributes['context'] ) |
| 135 |
: null; |
| 136 |
|
| 137 |
if ( null !== $context ) { |
| 138 |
return $context; |
| 139 |
} |
| 140 |
|
| 141 |
$args = [ 'widget_type' => 'shortcode' ]; |
| 142 |
|
| 143 |
$enabled = $this->enabled_actions(); |
| 144 |
if ( null !== $enabled ) { |
| 145 |
$args['enabled_actions'] = $enabled; |
| 146 |
} |
| 147 |
|
| 148 |
// Blank means inherit, so an unset attribute is left out entirely rather |
| 149 |
// than passed as an empty string. |
| 150 |
foreach ( [ 'button_label', 'prompt_template' ] as $key ) { |
| 151 |
$value = (string) $this->attributes[ $key ]; |
| 152 |
if ( '' !== trim( $value ) ) { |
| 153 |
$args[ $key ] = $value; |
| 154 |
} |
| 155 |
} |
| 156 |
|
| 157 |
return $args; |
| 158 |
} |
| 159 |
|
| 160 |
/** |
| 161 |
* Turn the `actions` allow-list into the tri-state map render() expects. |
| 162 |
* |
| 163 |
* An author writing the shortcode by hand means "show exactly these", so a |
| 164 |
* non-empty list forces every known action on or off explicitly. An omitted |
| 165 |
* attribute returns null, leaving every action to its global setting. |
| 166 |
* |
| 167 |
* @return array<string, bool>|null |
| 168 |
*/ |
| 169 |
protected function enabled_actions() { |
| 170 |
$list = trim( (string) $this->attributes['actions'] ); |
| 171 |
|
| 172 |
if ( '' === $list ) { |
| 173 |
return null; |
| 174 |
} |
| 175 |
|
| 176 |
$allowed = array_filter( array_map( 'trim', explode( ',', $list ) ) ); |
| 177 |
$enabled = []; |
| 178 |
|
| 179 |
foreach ( betterdocs()->ai_actions->catalog() as $action ) { |
| 180 |
$enabled[ $action['id'] ] = in_array( $action['id'], $allowed, true ); |
| 181 |
} |
| 182 |
|
| 183 |
return $enabled; |
| 184 |
} |
| 185 |
} |
| 186 |
|