diff --git a/breznflow/assets/renderer.js b/breznflow/assets/renderer.js index 695ff06..41182cf 100644 --- a/breznflow/assets/renderer.js +++ b/breznflow/assets/renderer.js @@ -1520,9 +1520,18 @@ function init() { if (typeof breznflowData === 'undefined' || !Array.isArray(breznflowData)) return; + // Defensive guard: if any filter re-ran the shortcode, breznflowData may + // contain duplicate entries pointing at the same container. Mounting twice + // would stack two full UIs via appendChild. Track mounted containers and + // skip repeats — independent of server-side dedupe. + const mounted = new WeakSet(); + for (const data of breznflowData) { - const container = document.getElementById('breznflow-wrap-' + data.id); + const domId = data.dom_id || ('breznflow-wrap-' + data.id); + const container = document.getElementById(domId); if (!container) continue; + if (mounted.has(container)) continue; + mounted.add(container); try { const renderer = new BreznFlowRenderer(data); diff --git a/breznflow/breznflow.php b/breznflow/breznflow.php index b35b56c..e6a205e 100644 --- a/breznflow/breznflow.php +++ b/breznflow/breznflow.php @@ -8,7 +8,7 @@ * Plugin Name: BreznFlow * Plugin URI: https://breznflow.com/ * Description: Display n8n automation workflows with an interactive SVG diagram, node detail panel, and sensitive data masking. - * Version: 1.0.2 + * Version: 1.0.3 * Requires at least: 6.0 * Requires PHP: 8.0 * Author: NoSchmarrn.dev @@ -23,7 +23,7 @@ if ( ! defined( 'ABSPATH' ) ) { exit; } -define( 'BREZNFLOW_VERSION', '1.0.2' ); +define( 'BREZNFLOW_VERSION', '1.0.3' ); define( 'BREZNFLOW_FILE', __FILE__ ); define( 'BREZNFLOW_DIR', plugin_dir_path( __FILE__ ) ); define( 'BREZNFLOW_URL', plugin_dir_url( __FILE__ ) ); diff --git a/breznflow/includes/Shortcode.php b/breznflow/includes/Shortcode.php index 1b4eb57..0196b1c 100644 --- a/breznflow/includes/Shortcode.php +++ b/breznflow/includes/Shortcode.php @@ -39,6 +39,31 @@ class Shortcode { */ private static bool $assets_enqueued = false; + /** + * Monotonic counter for unique wrapper DOM IDs within a request. + * + * @var int + */ + private static int $instance_counter = 0; + + /** + * Fingerprints of already-rendered shortcode invocations (post_id + resolved settings). + * Used to silently skip re-entrant passes triggered by plugins like Easy Table of Contents, + * which run the_content filters a second time to scan for headings. + * + * @var array + */ + private static array $fingerprints = array(); + + /** + * Post IDs that have already emitted the share-anchor span in this request. + * The anchor id="breznflow-" must be unique in the DOM, so only the + * first instance per post emits it; later instances get a wrapper only. + * + * @var array + */ + private static array $anchored_posts = array(); + /** * Registers the shortcode and footer hook. * @@ -154,7 +179,27 @@ class Shortcode { $max_code_lines = '' !== $atts['max_code_lines'] ? max( 1, (int) $atts['max_code_lines'] ) : (int) $settings['max_code_lines']; - // Increment view count. + // Re-entry guard: plugins like Easy Table of Contents run the_content filters + // twice to scan for headings, which re-executes this shortcode. The returned + // HTML is often deduplicated by the outer filter, but the static $render_queue + // below would still accumulate a duplicate entry and the JS renderer would + // mount the workflow twice onto the same container. Hashing post_id + fully + // resolved render settings lets us silently skip those re-entrant passes + // while still allowing legitimate multi-embed (same post, different atts). + $fingerprint = md5( + (string) $post_id . '|' . + $mode . '|' . (string) $zoom . '|' . (string) $show_title . '|' . + (string) $show_infobox . '|' . (string) $allow_download . '|' . + (string) $show_minimap . '|' . (string) $max_code_lines . '|' . + $theme . '|' . (string) $allow_share . '|' . (string) $allow_embed . '|' . + (string) $allow_get_json + ); + if ( isset( self::$fingerprints[ $fingerprint ] ) ) { + return ''; + } + self::$fingerprints[ $fingerprint ] = true; + + // Increment view count (after dedupe check, so re-entrant scans don't overcount). ViewCounter::increment( $post_id ); // Enqueue assets once. @@ -163,9 +208,15 @@ class Shortcode { self::$assets_enqueued = true; } + // Unique per-instance DOM id so multiple embeds of the same workflow coexist. + ++self::$instance_counter; + $instance_id = $post_id . '-' . self::$instance_counter; + $dom_id = 'breznflow-wrap-' . $instance_id; + // Queue workflow data for JS output. self::$render_queue[] = array( 'id' => $post_id, + 'dom_id' => $dom_id, 'workflow' => $workflow, 'mode' => $mode, 'zoom' => $zoom ? $zoom : 100, @@ -209,10 +260,18 @@ class Shortcode { ); $html .= InfoBoxBuilder::build( $categorized ); } else { + // Anchor span is only emitted for the first instance per post so the + // id="breznflow-" target stays unique; legacy deep-links keep working. + $anchor_html = ''; + if ( ! isset( self::$anchored_posts[ $post_id ] ) ) { + $anchor_html = ''; + self::$anchored_posts[ $post_id ] = true; + } + $html .= '
' - . '' - . '
' . '
' . '
'; diff --git a/breznflow/readme.txt b/breznflow/readme.txt index 1fcf0af..a7ecb1e 100644 --- a/breznflow/readme.txt +++ b/breznflow/readme.txt @@ -3,7 +3,7 @@ Contributors: mifupadev Tags: n8n, workflow, automation, diagram, svg Requires at least: 6.0 Tested up to: 6.9 -Stable tag: 1.0.2 +Stable tag: 1.0.3 Requires PHP: 8.0 License: GPL-2.0-or-later License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -16,6 +16,12 @@ BreznFlow turns n8n workflow JSON exports into interactive, zoomable SVG diagram The plugin was built for mifupa.com, a personal blog where n8n automations are documented regularly. Screenshots get outdated. Embedding the n8n editor is impractical. BreznFlow solves this: one shortcode, one interactive diagram, zero external dependencies. += Learn more = + +* Website: breznflow.com +* FAQ: breznflow.com/faq +* Live demo: breznflow.com/demo + = At a glance = * Renders n8n workflow JSON as interactive SVG diagrams with zoom, pan, and click @@ -158,6 +164,12 @@ For security, requests to private and internal network addresses are blocked: lo == Changelog == += 1.0.3 = +* Fixed double rendering when "Easy Table of Contents" (or any plugin that re-runs `the_content` filters) is active. The shortcode now silently deduplicates re-entrant invocations via a fingerprint of post id + resolved render settings. +* Wrapper `id` is now unique per instance (`breznflow-wrap--`), enabling multiple embeds of the same workflow with different attributes in one post. +* Anchor span `id="breznflow-"` is emitted only for the first instance per post to keep the DOM valid and preserve existing share links. +* Renderer now guards against mounting twice onto the same container. + = 1.0.2 = * Fixed WordPress.org plugin review issues. * Embed page now uses wp_enqueue_style/wp_enqueue_script with wp_head/wp_footer instead of direct HTML tags. @@ -196,6 +208,9 @@ For security, requests to private and internal network addresses are blocked: lo == Upgrade Notice == += 1.0.3 = +Fixes duplicate workflow rendering when "Easy Table of Contents" is active, and enables reliable multi-embed of the same workflow in one post. + = 1.0.2 = Fixes WordPress.org plugin review issues: proper asset enqueueing, nonce verification, input sanitization, and output escaping.