Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/Console/Command/Hyva/CompatibilityCheckCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ private function runScan(
}

// Display summary
$this->displaySummary($results);
$this->displaySummary($results['summary']);

// Display recommendations if there are issues
if ($results['hasIncompatibilities']) {
Expand Down Expand Up @@ -336,13 +336,11 @@ private function displayDetailedIssues(array $results): void
/**
* Display summary statistics
*
* @param array $results
* @phpstan-param array{summary: CheckSummary} $results
* @param array $summary
* @phpstan-param CheckSummary $summary
*/
private function displaySummary(array $results): void
private function displaySummary(array $summary): void
{
$summary = $results['summary'];

$this->io->section('Summary');

$summaryData = [
Expand Down
82 changes: 40 additions & 42 deletions src/Model/TemplateEngine/Decorator/InspectorHints.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace OpenForgeProject\MageForge\Model\TemplateEngine\Decorator;

use Magento\Framework\Escaper;
use Magento\Framework\Filesystem\Driver\File;
use Magento\Framework\Math\Random;
use Magento\Framework\View\Element\AbstractBlock;
Expand All @@ -29,6 +30,7 @@ class InspectorHints implements TemplateEngineInterface
* @param Random $random
* @param BlockCacheCollector $cacheCollector
* @param File $fileDriver
* @param Escaper $escaper
* @param string[] $excludedClassPrefixes Block class prefixes to skip inspector wrapping for
* @param string[] $excludedTemplatePaths Template path substrings to skip inspector wrapping for
*/
Expand All @@ -38,6 +40,7 @@ public function __construct(
private readonly Random $random,
private readonly BlockCacheCollector $cacheCollector,
private readonly File $fileDriver,
private readonly Escaper $escaper,
private readonly array $excludedClassPrefixes = [],
private readonly array $excludedTemplatePaths = [],
) {
Expand Down Expand Up @@ -97,20 +100,6 @@ private function isExcludedTemplate(string $templateFile): bool
return false;
}

/**
* Check if rendered HTML contains wire attributes (Magewire/Livewire components)
*
* Wrapping these in HTML comments breaks wire:id injection which relies on
* finding the first root element via regex.
*
* @param string $html
* @return bool
*/
private function containsWireAttributes(string $html): bool
{
return str_contains($html, 'wire:id=') || str_contains($html, 'wire:initial-data=');
}

/**
* Insert inspector data attributes into the rendered block contents
*
Expand All @@ -137,27 +126,10 @@ public function render(BlockInterface $block, $templateFile, array $dictionary =
}

// Skip inspector wrapping for templates in excluded paths (e.g. /magewire/ directories).
// Magewire injects wire:id AFTER the template engine returns via regex on the root element.
// Wrapping the output in HTML comments before that element breaks the injection.
if ($this->isExcludedTemplate($templateFile)) {
return $result;
}

// Skip inspector wrapping for Magewire component blocks.
// Magewire sets a 'magewire' data key on the block before rendering and injects wire:id
// via regex AFTER the template engine returns. Wrapping the output in HTML comments
// shifts the offset used by insertAttributesIntoHtmlRoot(), causing broken components.
// Soft dependency: hasData() is a Magento DataObject method, not a Magewire class.
if (method_exists($block, 'hasData') && $block->hasData('magewire')) {
return $result;
}

// Skip inspector wrapping if the rendered HTML contains wire attributes (Magewire/Livewire).
// This catches container blocks whose children have already been rendered with wire attributes.
if ($this->containsWireAttributes($result)) {
return $result;
}

// Only inject attributes if there's actual HTML content
if (empty(trim($result))) {
return $result;
Expand All @@ -177,7 +149,13 @@ public function render(BlockInterface $block, $templateFile, array $dictionary =
}

/**
* Inject MageForge inspector comment markers into HTML
* Inject MageForge inspector data attributes into the first root HTML element
*
* Injects data-mageforge-id and data-mageforge-block on the opening tag of the
* first HTML element in the output. If the content does not start with an HTML
* element (e.g. a plain URL or text fragment used inside an href attribute by a
* parent PageBuilder template), injection is skipped entirely to avoid corrupting
* the surrounding markup.
*
* @param string $html
* @param BlockInterface $block
Expand Down Expand Up @@ -213,6 +191,9 @@ private function injectInspectorAttributes(
$cacheMetrics = $this->cacheCollector->getCacheInfo($block);
$formattedMetrics = $this->cacheCollector->formatMetricsForJson($renderMetrics, $cacheMetrics);

// Detect CMS block identifier (e.g. for PageBuilder blocks rendered via Magento\Cms\Block\Block)
$cmsBlockId = method_exists($block, 'getBlockId') ? (string) $block->getBlockId() : '';
Comment thread
dermatz marked this conversation as resolved.

// Build metadata as JSON
$metadata = [
'id' => $wrapperId,
Expand All @@ -223,29 +204,46 @@ private function injectInspectorAttributes(
'parent' => $parentBlock,
'alias' => $blockAlias,
'override' => $isOverride,
'cmsBlockId' => $cmsBlockId,
'performance' => $formattedMetrics['performance'],
'cache' => $formattedMetrics['cache'],
];

// JSON encode with proper escaping for HTML comments
$jsonMetadata = json_encode($metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);

if ($jsonMetadata === false) {
return $html;
}

// Escape any comment terminators in JSON to prevent breaking out of comment
$jsonMetadata = str_replace('-->', '-->', $jsonMetadata);

// Wrap content with comment markers
$wrappedHtml = sprintf(
"<!-- MAGEFORGE_START %s -->\n%s\n<!-- MAGEFORGE_END %s -->",
$jsonMetadata,
// Escape all characters that need HTML-encoding so the JSON can be safely
// embedded in an HTML attribute. escapeHtml handles &, <, > and quotes.
// The browser automatically decodes HTML entities when getAttribute() is called,
// so JSON.parse() on the JS side will receive the correct string.
$safeJson = $this->escaper->escapeHtml($jsonMetadata);

// Inject data-mageforge-* attributes on the first root HTML element.
// This avoids HTML comment nodes which corrupt markup when block output is
// embedded inside HTML attribute values (e.g. PageBuilder URL blocks in href="...").
$replaced = false;
$result = preg_replace_callback(
'/^(\s*<[a-zA-Z][a-zA-Z0-9-]*)/s',
function (array $matches) use ($wrapperId, $safeJson, &$replaced): string {
$replaced = true;
return $matches[0]
Comment thread
dermatz marked this conversation as resolved.
. ' data-mageforge-id="' . $wrapperId . '"'
. ' data-mageforge-block="' . $safeJson . '"';
},
$html,
$wrapperId,
1,
);
Comment thread
dermatz marked this conversation as resolved.

return $wrappedHtml;
// If content doesn't start with an HTML element (e.g. plain text, URLs),
// skip injection to avoid corrupting attribute values in parent templates.
if (!$replaced || $result === null) {
return $html;
}

return $result;
}

/**
Expand Down
36 changes: 36 additions & 0 deletions src/Model/TemplateEngine/Decorator/InspectorHintsFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace OpenForgeProject\MageForge\Model\TemplateEngine\Decorator;

use Magento\Framework\ObjectManagerInterface;

/**
* Factory for creating InspectorHints instances via the ObjectManager.
*/
class InspectorHintsFactory
{
/**
* @param ObjectManagerInterface $objectManager
*/
public function __construct(
private readonly ObjectManagerInterface $objectManager,
) {
}

// phpcs:disable Magento2.Annotation.MethodArguments.ArgumentMissing
/**
* Create a new InspectorHints instance.
*
* @param array<string, mixed> $data
* @return InspectorHints
*/
// phpcs:enable Magento2.Annotation.MethodArguments.ArgumentMissing
public function create(array $data = []): InspectorHints
{
/** @var InspectorHints $instance */
$instance = $this->objectManager->create(InspectorHints::class, $data);
return $instance;
}
}
12 changes: 11 additions & 1 deletion src/view/frontend/web/css/inspector.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,17 @@
align-items: center;
gap: 8px;
font-family: var(--mageforge-font-family);
font-size: 0.75rem;
font-size: 14px;
font-weight: 600;
line-height: 1;
white-space: nowrap;
text-transform: none;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(8px);
letter-spacing: 0.025em;
vertical-align: middle;
text-decoration: none;
}

.mageforge-inspector-float-button:hover {
Expand All @@ -109,6 +114,11 @@
transform: translateY(0);
}

.mageforge-inspector-float-button:focus-visible {
outline: 2px solid rgba(255, 255, 255, 0.8);
outline-offset: 2px;
}

.mageforge-inspector-float-button.mageforge-active {
background: linear-gradient(135deg, var(--mageforge-color-green) 0%, var(--mageforge-color-green-dark) 100%);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2), 0 8px 20px rgba(16, 185, 129, 0.5);
Expand Down
28 changes: 22 additions & 6 deletions src/view/frontend/web/js/inspector.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@ function _registerMageforgeInspector() {
pageTimings: null,
performanceObservers: [],

// Block detection cache
cachedBlocks: null,
lastBlocksCacheTime: 0,

// Window event handler refs (for cleanup)
_inspectorStateHandler: null,

Expand Down Expand Up @@ -109,6 +105,7 @@ function _registerMageforgeInspector() {
this._inspectorStateHandler = (e) => {
if (this._inspectorFloatButton) {
this._inspectorFloatButton.classList.toggle('mageforge-active', e.detail.active);
this._inspectorFloatButton.setAttribute('aria-pressed', e.detail.active ? 'true' : 'false');
}
};
window.addEventListener('mageforge:toolbar:inspector-state', this._inspectorStateHandler);
Expand All @@ -123,10 +120,13 @@ function _registerMageforgeInspector() {
},

_createInspectorFloatButton() {
const btn = document.createElement('button');
// Use div instead of button to avoid Luma/theme button CSS overrides
const btn = document.createElement('div');
btn.className = 'mageforge-inspector-float-button';
btn.type = 'button';
btn.title = 'Activate Inspector (Ctrl+Shift+I)';
btn.setAttribute('role', 'button');
btn.setAttribute('tabindex', '0');
btn.setAttribute('aria-pressed', 'false');
btn.innerHTML = `
Comment thread
dermatz marked this conversation as resolved.
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor" height="20" width="20">
<g stroke-width="0"></g>
Expand All @@ -142,6 +142,22 @@ function _registerMageforgeInspector() {
e.stopPropagation();
this.toggleInspector();
};
btn.onkeydown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
e.stopPropagation();
this.toggleInspector();
}
if (e.key === ' ') {
e.preventDefault(); // prevent page scroll on Space
}
};
btn.onkeyup = (e) => {
if (e.key === ' ') {
e.stopPropagation();
this.toggleInspector();
}
};
Comment thread
dermatz marked this conversation as resolved.
return btn;
},

Expand Down
Loading
Loading