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>
317 lines
11 KiB
PHP
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 );
|
|
}
|
|
}
|
|
}
|