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
1 change: 1 addition & 0 deletions php-transformer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"test:unit": [
"php tests/unit/subtree-classifier.php",
"php tests/unit/custom-block-generator.php",
"php tests/unit/description-list-block.php",
"php tests/unit/css-value-splitter.php",
"php tests/unit/background-image-extractor.php",
"php tests/unit/css-stylesheet-transformer.php",
Expand Down
24 changes: 18 additions & 6 deletions php-transformer/src/ArtifactCompiler/ArtifactCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,20 @@ public function compile(array $artifact): TransformerResult
$companionPluginPayloadBuilder = new CompanionPluginPayload();
$normalized['files'] = $this->withStylesheetOccurrenceAssets($html, $entryPath, $normalized['files']);
$entryBlocks = $this->compileEntryBlocks($html, $entryPath, $normalized['files'], $companionPluginPayloadBuilder->blockNamespace($artifact));
$compiledHtmlDocuments = $this->compileHtmlSourceDocuments($normalized['files'], $entryPath);
$compiledHtmlDocuments = $this->compileHtmlSourceDocuments($normalized['files'], $entryPath, $companionPluginPayloadBuilder->blockNamespace($artifact));
$authorStylesheetProjections = $entryBlocks['author_stylesheet_projections'];
$allDiagnostics = $this->entryTransformDiagnostics($entryBlocks['diagnostics'], $entryPath);
$allFallbacks = $entryBlocks['fallbacks'];
$allGeneratedBlocks = $entryBlocks['generated_blocks'];
$allGutenbergGaps = $entryBlocks['gutenberg_gaps'];
foreach ( $compiledHtmlDocuments as $sourcePath => $compiledHtmlDocument ) {
$authorStylesheetProjections = array_merge($authorStylesheetProjections, $compiledHtmlDocument['author_stylesheet_projections'] ?? array());
$allDiagnostics = array_merge($allDiagnostics, $this->entryTransformDiagnostics($compiledHtmlDocument['diagnostics'] ?? array(), (string) $sourcePath));
$allFallbacks = array_merge($allFallbacks, $compiledHtmlDocument['fallbacks'] ?? array());
$allGeneratedBlocks = array_merge($allGeneratedBlocks, $compiledHtmlDocument['generated_blocks'] ?? array());
$allGutenbergGaps = array_merge($allGutenbergGaps, $compiledHtmlDocument['gutenberg_gaps'] ?? array());
}
$allGutenbergGaps = $this->dedupeRows($allGutenbergGaps);
$normalized['runtime_declarations'] = $this->runtimeDeclarationsFromFallbacks($normalized['runtime_declarations'], $allFallbacks, $entryPath, $normalized['files']);
$runtimeIslandPackage = ( new RuntimeIslandPackageBuilder() )->fromRuntimeIslands($entryBlocks['runtime_islands'], $normalized['files'], $entryPath);
$normalized['files'] = $this->applyAuthorStylesheetProjections($normalized['files'], $authorStylesheetProjections, $entryBlocks['author_stylesheet_projections']);
Expand Down Expand Up @@ -125,8 +130,11 @@ public function compile(array $artifact): TransformerResult
),
);
$sourceReports['compiled_site'] = $this->compiledSiteReport($normalized, $entryPath, $documents['documents'], $assets, $blockTypes, $serializedBlocks, $entryBlocks['shell_artifacts'], $compiledHtmlDocuments);
if ( array() !== $allGutenbergGaps ) {
$sourceReports['gutenberg_gaps'] = $allGutenbergGaps;
}
$sourceReports['materialization_plan'] = ( new MaterializationPlanBuilder() )->fromCompiledSite($sourceReports['compiled_site']);
$companionPluginPayload = $companionPluginPayloadBuilder->fromBlockTypes($blockTypes, $normalized['files'], $artifact, $entryBlocks['generated_blocks'], $runtimeIslandPackage);
$companionPluginPayload = $companionPluginPayloadBuilder->fromBlockTypes($blockTypes, $normalized['files'], $artifact, $allGeneratedBlocks, $runtimeIslandPackage);
if ( array() !== $companionPluginPayload ) {
$sourceReports['companion_plugin_payload'] = $companionPluginPayload;
}
Expand Down Expand Up @@ -375,7 +383,7 @@ public function compileFragment(string $content, string $source = 'fragment', st

/**
* @param array<int, array<string, mixed>> $files
* @return array{blocks: array<int, array<string, mixed>>, serialized_blocks: string, diagnostics: array<int, array<string, mixed>>, fallbacks: array<int, array<string, mixed>>, assets: array<int, array<string, mixed>>, runtime_islands: array<int, array<string, mixed>>, generated_blocks: array<int, array<string, mixed>>, interaction_candidates: array<int, array<string, mixed>>, superseded_selectors: array<int, string>, author_stylesheet_projections: array<int, array<string, mixed>>, shell_artifacts: array<int, array<string, mixed>>}
* @return array{blocks: array<int, array<string, mixed>>, serialized_blocks: string, diagnostics: array<int, array<string, mixed>>, fallbacks: array<int, array<string, mixed>>, assets: array<int, array<string, mixed>>, runtime_islands: array<int, array<string, mixed>>, generated_blocks: array<int, array<string, mixed>>, gutenberg_gaps: array<int, array<string, mixed>>, interaction_candidates: array<int, array<string, mixed>>, superseded_selectors: array<int, string>, author_stylesheet_projections: array<int, array<string, mixed>>, shell_artifacts: array<int, array<string, mixed>>}
*/
private function compileEntryBlocks(string $html, string $entryPath, array $files, string $generatedBlockNamespace = ''): array
{
Expand All @@ -389,6 +397,7 @@ private function compileEntryBlocks(string $html, string $entryPath, array $file
'assets' => $result['assets'],
'runtime_islands' => $result['runtime_islands'],
'generated_blocks' => $result['generated_blocks'],
'gutenberg_gaps' => $result['gutenberg_gaps'],
'interaction_candidates' => $result['interaction_candidates'],
'superseded_selectors' => $result['superseded_selectors'],
'author_stylesheet_projections' => $result['author_stylesheet_projections'],
Expand All @@ -398,7 +407,7 @@ private function compileEntryBlocks(string $html, string $entryPath, array $file

/**
* @param array<int, array<string, mixed>> $files
* @return array{blocks: array<int, array<string, mixed>>, serialized_blocks: string, diagnostics: array<int, array<string, mixed>>, fallbacks: array<int, array<string, mixed>>, assets: array<int, array<string, mixed>>, runtime_islands: array<int, array<string, mixed>>, generated_blocks: array<int, array<string, mixed>>, interaction_candidates: array<int, array<string, mixed>>, superseded_selectors: array<int, string>, author_stylesheet_projections: array<int, array<string, mixed>>, shell_artifacts: array<int, array<string, mixed>>}
* @return array{blocks: array<int, array<string, mixed>>, serialized_blocks: string, diagnostics: array<int, array<string, mixed>>, fallbacks: array<int, array<string, mixed>>, assets: array<int, array<string, mixed>>, runtime_islands: array<int, array<string, mixed>>, generated_blocks: array<int, array<string, mixed>>, gutenberg_gaps: array<int, array<string, mixed>>, interaction_candidates: array<int, array<string, mixed>>, superseded_selectors: array<int, string>, author_stylesheet_projections: array<int, array<string, mixed>>, shell_artifacts: array<int, array<string, mixed>>}
*/
private function compileHtmlDocumentBlocks(string $html, string $sourcePath, array $files, string $sourceScope, string $generatedBlockNamespace = '', bool $extractGlobalShell = false): array
{
Expand All @@ -411,6 +420,7 @@ private function compileHtmlDocumentBlocks(string $html, string $sourcePath, arr
'assets' => array(),
'runtime_islands' => array(),
'generated_blocks' => array(),
'gutenberg_gaps' => array(),
'interaction_candidates' => array(),
'superseded_selectors' => array(),
'author_stylesheet_projections' => array(),
Expand All @@ -427,6 +437,7 @@ private function compileHtmlDocumentBlocks(string $html, string $sourcePath, arr
'assets' => array(),
'runtime_islands' => array(),
'generated_blocks' => array(),
'gutenberg_gaps' => array(),
'interaction_candidates' => array(),
'superseded_selectors' => array(),
'author_stylesheet_projections' => array(),
Expand Down Expand Up @@ -460,6 +471,7 @@ private function compileHtmlDocumentBlocks(string $html, string $sourcePath, arr
$files
),
'generated_blocks' => is_array($result['source_reports']['generated_blocks'] ?? null) ? $result['source_reports']['generated_blocks'] : array(),
'gutenberg_gaps' => is_array($result['source_reports']['gutenberg_gaps'] ?? null) ? $result['source_reports']['gutenberg_gaps'] : array(),
'interaction_candidates' => is_array($result['source_reports']['interaction_candidates'] ?? null) ? $result['source_reports']['interaction_candidates'] : array(),
'superseded_selectors' => array_values(array_filter(
is_array($result['source_reports']['superseded_selectors'] ?? null) ? $result['source_reports']['superseded_selectors'] : array(),
Expand Down Expand Up @@ -1022,7 +1034,7 @@ private function applyAuthorStylesheetProjections(array $files, array $projectio
* @param array<int, array<string, mixed>> $files
* @return array<string, array<string, mixed>>
*/
private function compileHtmlSourceDocuments(array $files, string $entryPath): array
private function compileHtmlSourceDocuments(array $files, string $entryPath, string $generatedBlockNamespace = ''): array
{
$documents = array();
foreach ( $files as $file ) {
Expand All @@ -1033,7 +1045,7 @@ private function compileHtmlSourceDocuments(array $files, string $entryPath): ar
if ( '' === $path || $entryPath === $path ) {
continue;
}
$documents[$path] = $this->compileHtmlDocumentBlocks((string) ($file['content'] ?? ''), $path, $files, 'artifact-document', '', true);
$documents[$path] = $this->compileHtmlDocumentBlocks((string) ($file['content'] ?? ''), $path, $files, 'artifact-document', $generatedBlockNamespace, true);
}
return $documents;
}
Expand Down
128 changes: 128 additions & 0 deletions php-transformer/src/HtmlToBlocks/DescriptionListBlockGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks;

/**
* Builds the static companion block that fills Gutenberg's description-list gap.
*
* Gutenberg issue #4880 remains unresolved; the proposed core implementation in
* stalled PR #20760 is not available to generated sites.
*/
final class DescriptionListBlockGenerator
{
public const NAME = 'blocks-engine/description-list';

/** @return array<string, mixed> */
public function blockJson(): array
{
return array(
'apiVersion' => 3,
'name' => self::NAME,
'title' => 'Description List',
'category' => 'text',
'description' => 'A semantic description list with terms and descriptions.',
'editorScript' => 'file:./index.js',
'attributes' => array(
'className' => array( 'type' => 'string', 'default' => '' ),
'style' => array( 'type' => 'string', 'default' => '' ),
'groups' => array( 'type' => 'array', 'default' => array() ),
),
'supports' => array( 'html' => false ),
);
}

/** @return array<string, string> */
public function assets(): array
{
$script = <<<'JS'
( function( blocks, blockEditor, element ) {
var createElement = element.createElement;
var RawHTML = element.RawHTML;
var RichText = blockEditor.RichText;
var useEffect = element.useEffect;
var attributes = __BLOCK_ATTRIBUTES__;
function escapeAttribute( value ) { return String( value || '' ).replace( /&/g, '&amp;' ).replace( /"/g, '&quot;' ).replace( /</g, '&lt;' ).replace( />/g, '&gt;' ); }
function safeCssText( value ) {
var probe = document.createElement( 'span' );
probe.setAttribute( 'style', value || '' );
return probe.style.cssText;
}
function markupAttributes( item ) {
var output = '';
if ( item.className ) { output += ' class="' + escapeAttribute( item.className ) + '"'; }
if ( item.style ) { output += ' style="' + escapeAttribute( item.style ) + '"'; }
return output;
}
function markup( blockAttributes ) {
var output = '<dl' + markupAttributes( blockAttributes ) + '>';
( blockAttributes.groups || [] ).forEach( function( group ) {
( group.terms || [] ).forEach( function( item ) { output += '<dt' + markupAttributes( item ) + '>' + ( item.content || '' ) + '</dt>'; } );
( group.descriptions || [] ).forEach( function( item ) { output += '<dd' + markupAttributes( item ) + '>' + ( item.content || '' ) + '</dd>'; } );
} );
return output + '</dl>';
}
function updateItem( props, groupIndex, collection, itemIndex, content ) {
var groups = ( props.attributes.groups || [] ).map( function( group ) {
return {
terms: ( group.terms || [] ).map( function( item ) { return Object.assign( {}, item ); } ),
descriptions: ( group.descriptions || [] ).map( function( item ) { return Object.assign( {}, item ); } )
};
} );
groups[ groupIndex ][ collection ][ itemIndex ].content = content;
props.setAttributes( { groups: groups } );
}
function edit( props ) {
var children = [];
var scope = 'be-description-list-' + String( props.clientId || 'block' ).replace( /[^a-zA-Z0-9_-]/g, '' );
var rules = safeCssText( props.attributes.style ) ? '.' + scope + '{' + safeCssText( props.attributes.style ) + '}' : '';
( props.attributes.groups || [] ).forEach( function( group, groupIndex ) {
( group.terms || [] ).forEach( function( item, itemIndex ) {
var key = 'term-' + groupIndex + '-' + itemIndex;
var css = safeCssText( item.style );
if ( css ) { rules += '.' + scope + ' [data-be-description-list-item="' + key + '"]{' + css + '}'; }
children.push( createElement( RichText, { tagName: 'dt', value: item.content || '', className: item.className || undefined, 'data-be-description-list-item': key, key: key, onChange: function( content ) { updateItem( props, groupIndex, 'terms', itemIndex, content ); } } ) );
} );
( group.descriptions || [] ).forEach( function( item, itemIndex ) {
var key = 'description-' + groupIndex + '-' + itemIndex;
var css = safeCssText( item.style );
if ( css ) { rules += '.' + scope + ' [data-be-description-list-item="' + key + '"]{' + css + '}'; }
children.push( createElement( RichText, { tagName: 'dd', value: item.content || '', className: item.className || undefined, 'data-be-description-list-item': key, key: key, onChange: function( content ) { updateItem( props, groupIndex, 'descriptions', itemIndex, content ); } } ) );
} );
} );
useEffect( function() {
if ( ! rules ) { return undefined; }
var sheet = document.createElement( 'style' );
sheet.textContent = rules;
document.head.appendChild( sheet );
return function() { sheet.remove(); };
}, [ rules ] );
return createElement( 'dl', { className: [ props.attributes.className, scope ].filter( Boolean ).join( ' ' ) }, children );
}
function save( props ) { return createElement( RawHTML, null, markup( props.attributes ) ); }
blocks.registerBlockType( 'blocks-engine/description-list', { attributes: attributes, supports: { html: false }, edit: edit, save: save } );
} )( window.wp.blocks, window.wp.blockEditor, window.wp.element );
JS;

return array(
'index.asset.php' => <<<'PHP'
<?php return array( 'dependencies' => array( 'wp-blocks', 'wp-block-editor', 'wp-element' ), 'version' => '1.0.0' );
PHP,
'index.js' => str_replace(
'__BLOCK_ATTRIBUTES__',
json_encode($this->blockJson()['attributes'], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES),
$script
),
);
}

/** @return array<string, mixed> */
public function definition(): array
{
return array(
'name' => 'description-list',
'block_json' => $this->blockJson(),
'assets' => $this->assets(),
);
}
}
Loading
Loading