breznflow/includes/Shortcode.php
Michael fd83e4810b BreznFlow 1.0.0 — WordPress.org submission
Initial public release of BreznFlow, an n8n workflow renderer for WordPress.
Fully PHPCS-compliant (WordPress Coding Standards), security-hardened,
and ready for WordPress.org plugin review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 11:27:36 +00:00

317 lines
11 KiB
PHP

<?php
/**
* Shortcode handler for rendering workflows on the frontend.
*
* @package BreznFlow
* @since 1.0.0
*/
namespace BreznFlow;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use BreznFlow\Features\NodeTypeRegistry;
use BreznFlow\Features\ThemeRegistry;
use BreznFlow\Features\ViewCounter;
use BreznFlow\Features\InfoBoxBuilder;
use BreznFlow\Features\NodeCategorizer;
use BreznFlow\Admin\SettingsPage;
/**
* Handles the [breznflow] shortcode, asset enqueueing, and JS data output.
*
* @since 1.0.0
*/
class Shortcode {
/**
* Accumulated workflow data for wp_localize_script output.
*
* @var array<int, array<string, mixed>>
*/
private static array $render_queue = array();
/**
* Whether frontend assets have been enqueued for this page load.
*
* @var bool
*/
private static bool $assets_enqueued = false;
/**
* Registers the shortcode and footer hook.
*
* @since 1.0.0
* @return void
*/
public function register(): void {
add_shortcode( 'breznflow', array( $this, 'render' ) );
add_action( 'wp_footer', array( $this, 'output_script_data' ), 1 );
}
/**
* Renders the [breznflow] shortcode.
*
* @param array $atts Shortcode attributes.
* @return string HTML output.
*/
public function render( $atts ): string {
$settings = SettingsPage::get_defaults();
$saved = get_option( 'breznflow_settings', array() );
$settings = array_merge( $settings, $saved );
$atts = shortcode_atts(
array(
'id' => 0,
'mode' => '',
'show_title' => '',
'show_infobox' => '',
'show_download' => '',
'show_minimap' => '',
'zoom' => '',
'max_code_lines' => '',
'preset' => '',
'show_share' => '',
'show_embed' => '',
'show_get_json' => '',
'theme' => '',
),
$atts,
'breznflow'
);
$post_id = (int) $atts['id'];
if ( $post_id <= 0 ) {
return '';
}
$post = get_post( $post_id );
if ( ! $post || 'breznflow_workflow' !== $post->post_type || 'publish' !== $post->post_status ) {
return '';
}
$raw_json = get_post_meta( $post_id, '_breznflow_raw_json', true );
if ( ! $raw_json ) {
return '';
}
$workflow = json_decode( $raw_json, true );
if ( ! is_array( $workflow ) ) {
return '';
}
// Resolve settings from meta, overridden by shortcode attrs.
$att_mode = $atts['mode'];
$meta_mode = get_post_meta( $post_id, '_breznflow_default_mode', true );
$mode = $att_mode ? $att_mode : ( $meta_mode ? $meta_mode : $settings['default_mode'] );
$mode = in_array( $mode, array( 'visual', 'info', 'compact' ), true ) ? $mode : 'visual';
$zoom = '' !== $atts['zoom']
? max( 10, min( 200, (int) $atts['zoom'] ) )
: (int) get_post_meta( $post_id, '_breznflow_default_zoom', true );
$show_title = '' !== $atts['show_title']
? (bool) $atts['show_title']
: (bool) get_post_meta( $post_id, '_breznflow_show_title', true );
$show_infobox = '' !== $atts['show_infobox']
? (bool) $atts['show_infobox']
: (bool) get_post_meta( $post_id, '_breznflow_show_infobox', true );
// Download: shortcode can only disable if meta has it enabled (not enable if meta disables).
$meta_download = (bool) get_post_meta( $post_id, '_breznflow_show_download', true );
$global_download = (bool) $settings['allow_download'];
$allow_download = $meta_download && $global_download;
if ( '' !== $atts['show_download'] && ! (bool) $atts['show_download'] ) {
$allow_download = false;
}
$allow_share = (bool) $settings['allow_share'];
if ( '' !== $atts['show_share'] && ! (bool) $atts['show_share'] ) {
$allow_share = false;
}
// Embed: dual-gate (global + per-post meta).
$meta_embed = (bool) get_post_meta( $post_id, '_breznflow_show_embed', true );
$allow_embed = $meta_embed && (bool) $settings['allow_embed'];
if ( '' !== $atts['show_embed'] && ! (bool) $atts['show_embed'] ) {
$allow_embed = false;
}
$allow_get_json = (bool) $settings['allow_get_json'];
if ( '' !== $atts['show_get_json'] && ! (bool) $atts['show_get_json'] ) {
$allow_get_json = false;
}
$allowed_themes = ThemeRegistry::get_theme_ids();
$att_theme = $atts['theme'];
$meta_theme = get_post_meta( $post_id, '_breznflow_default_theme', true );
$theme = $att_theme ? $att_theme : ( $meta_theme ? $meta_theme : ( $settings['default_theme'] ?? 'dark' ) );
$theme = in_array( $theme, $allowed_themes, true ) ? $theme : 'dark';
$meta_minimap = get_post_meta( $post_id, '_breznflow_show_minimap', true );
$show_minimap = '' !== $atts['show_minimap']
? (bool) $atts['show_minimap']
: ( '' !== $meta_minimap ? (bool) $meta_minimap : true );
$max_code_lines = '' !== $atts['max_code_lines'] ? max( 1, (int) $atts['max_code_lines'] ) : (int) $settings['max_code_lines'];
// Increment view count.
ViewCounter::increment( $post_id );
// Enqueue assets once.
if ( ! self::$assets_enqueued ) {
$this->enqueue_assets( $settings );
self::$assets_enqueued = true;
}
// Queue workflow data for JS output.
self::$render_queue[] = array(
'id' => $post_id,
'workflow' => $workflow,
'mode' => $mode,
'zoom' => $zoom ? $zoom : 100,
'autofit_threshold' => (int) ( $settings['autofit_threshold'] ?? 30 ),
'show_title' => $show_title,
'show_infobox' => $show_infobox,
'show_download' => $allow_download,
'show_minimap' => $show_minimap,
'max_code_lines' => $max_code_lines,
'download_label' => esc_html( $settings['download_label'] ),
'download_url' => $allow_download ? esc_url( add_query_arg( 'breznflow_download', $post_id, home_url( '/' ) ) ) : '',
'show_share' => $allow_share,
'show_embed' => $allow_embed,
'show_get_json' => $allow_get_json,
'permalink' => $allow_share ? esc_url( get_permalink() ) : '',
'anchor_id' => 'breznflow-' . $post_id,
'workflow_title' => esc_html( $post->post_title ),
'node_count' => (int) get_post_meta( $post_id, '_breznflow_node_count', true ),
'is_ai_powered' => (bool) get_post_meta( $post_id, '_breznflow_has_ai_nodes', true ),
'blog_name' => esc_html( get_bloginfo( 'name' ) ),
'blog_url' => esc_url( home_url( '/' ) ),
'embed_url' => $allow_embed ? esc_url( add_query_arg( 'breznflow_embed', $post_id, home_url( '/' ) ) ) : '',
'theme' => $theme,
);
// Build HTML placeholder.
$html = '';
if ( $show_title ) {
$html .= '<h3 class="breznflow-title">' . esc_html( $post->post_title ) . '</h3>';
}
if ( 'info' === $mode ) {
$node_summary = get_post_meta( $post_id, '_breznflow_node_summary', true );
$node_count = (int) get_post_meta( $post_id, '_breznflow_node_count', true );
$has_ai = (int) get_post_meta( $post_id, '_breznflow_has_ai_nodes', true );
$categorized = array(
'counts' => (array) json_decode( $node_summary ? $node_summary : '{}', true ),
'has_ai' => (bool) $has_ai,
'total' => $node_count,
);
$html .= InfoBoxBuilder::build( $categorized );
} else {
$html .= '<div style="position:relative">'
. '<span id="breznflow-' . esc_attr( (string) $post_id ) . '" '
. 'aria-hidden="true" style="position:absolute;top:-60px;left:0"></span>'
. '<div id="breznflow-wrap-' . esc_attr( (string) $post_id ) . '" '
. 'class="breznflow-embed" data-id="' . esc_attr( (string) $post_id ) . '">'
. '</div>'
. '</div>';
}
return $html;
}
/**
* Outputs accumulated workflow data as a single wp_localize_script call.
* Hooked to wp_footer priority 1 (before scripts).
*/
public function output_script_data(): void {
if ( empty( self::$render_queue ) ) {
return;
}
wp_localize_script( 'breznflow-renderer', 'breznflowData', self::$render_queue );
wp_localize_script( 'breznflow-renderer', 'breznflowIcons', NodeTypeRegistry::get_registry() );
wp_localize_script( 'breznflow-renderer', 'breznflowI18n', self::get_js_i18n() );
}
/**
* Returns translatable strings for the frontend renderer JS.
*
* @return array<string, string>
*/
public static function get_js_i18n(): array {
return array(
'share' => __( 'Share', 'breznflow' ),
'embed' => __( 'Embed', 'breznflow' ),
'getJson' => __( 'Get JSON', 'breznflow' ),
'copy' => __( 'Copy', 'breznflow' ),
'copied' => __( 'Copied!', 'breznflow' ),
'error' => __( 'Error', 'breznflow' ),
'close' => __( 'Close', 'breznflow' ),
'zoomIn' => __( 'Zoom in', 'breznflow' ),
'zoomOut' => __( 'Zoom out', 'breznflow' ),
'resetView' => __( 'Reset view', 'breznflow' ),
'fullscreen' => __( 'Fullscreen', 'breznflow' ),
'minimap' => __( 'Minimap', 'breznflow' ),
'highlightInDiagram' => __( 'Highlight in diagram', 'breznflow' ),
'articleLink' => __( 'Article Link', 'breznflow' ),
'anchorLink' => __( 'Workflow Anchor Link', 'breznflow' ),
'embedDesc' => __( 'Embed this workflow on any website:', 'breznflow' ),
'optionalParams' => __( 'Optional URL parameters:', 'breznflow' ),
'code' => __( 'Code', 'breznflow' ),
'credential' => __( 'Credential', 'breznflow' ),
'type' => __( 'Type', 'breznflow' ),
'aiPowered' => __( 'AI-powered', 'breznflow' ),
'more' => __( 'more', 'breznflow' ),
'node' => __( 'node', 'breznflow' ),
'nodes' => __( 'nodes', 'breznflow' ),
'line' => __( 'line', 'breznflow' ),
'lines' => __( 'lines', 'breznflow' ),
);
}
/**
* Outputs Schema.org HowTo structured data if enabled.
*/
public function maybe_output_schema(): void {
if ( ! is_singular() ) {
return;
}
$settings = get_option( 'breznflow_settings', array() );
if ( empty( $settings['schema_howto'] ) ) {
return;
}
// Schema output is handled per-shortcode; here we hook for future expansion.
}
/**
* Enqueues renderer assets.
*
* @since 1.0.0
* @param array $settings Plugin settings array (reserved for future use).
* @return void
*/
private function enqueue_assets( array $settings ): void {
wp_enqueue_style( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.css', array(), BREZNFLOW_VERSION );
wp_enqueue_script( 'breznflow-renderer', BREZNFLOW_URL . 'assets/renderer.js', array(), BREZNFLOW_VERSION, true );
// Enqueue built-in theme CSS files.
foreach ( ThemeRegistry::BUILTIN as $id => $name ) {
wp_enqueue_style(
'breznflow-theme-' . $id,
ThemeRegistry::get_builtin_url( $id ),
array( 'breznflow-renderer' ),
BREZNFLOW_VERSION
);
}
// Output custom themes as inline CSS.
$custom_css = ThemeRegistry::get_custom_theme_css();
if ( $custom_css ) {
wp_add_inline_style( 'breznflow-renderer', $custom_css );
}
}
}