Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
<?php
declare(strict_types=1);

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style;

use Automattic\BlocksEngine\PhpTransformer\Css\CssValueSplitter;
namespace Automattic\BlocksEngine\PhpTransformer\Css;

/** Conservative expansion of the ordinary (non-system) CSS font shorthand. */
final class CssFontShorthand
{
/** @return array<string, string> Empty when the value cannot safely be expanded. */
public static function expand(string $value, bool $allowFluidSize = false): array
/**
* @param int|null $familyOffset Receives the family start in the original value.
* @return array<string, string> Empty when the value cannot safely be expanded.
*/
public static function expand(string $value, bool $allowFluidSize = false, ?int &$familyOffset = null): array
{
$important = CssValueInspector::isImportant($value) ? ' !important' : '';
$familyOffset = null;
$leading = strlen($value) - strlen(ltrim($value));
$important = 1 === preg_match('/\s*!\s*important\s*$/i', $value) ? ' !important' : '';
$value = trim(preg_replace('/\s*!important\s*$/i', '', $value) ?? $value);
// Substitution and system/CSS-wide keywords require computed-value data.
if (!$allowFluidSize && preg_match('/\bvar\s*\(/i', $value)) return array();
Expand All @@ -36,6 +39,14 @@ public static function expand(string $value, bool $allowFluidSize = false): arra
if (array() === $tokens) return array();
$result['line-height'] = $lineHeight;
$result['font-family'] = implode(' ', $tokens);
// Locate the remaining tokens in the source rather than rebuilding
// the prefix: shorthand whitespace and spelling must survive binding.
$offset = strlen($value);
foreach (array_reverse($tokens) as $familyToken) {
$offset = strrpos(substr($value, 0, $offset), $familyToken);
if (false === $offset) return array();
}
$familyOffset = $leading + $offset;
foreach ($result as &$longhand) $longhand .= $important;
unset($longhand);
return $result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style;

use Automattic\BlocksEngine\PhpTransformer\Css\CssColor;
use Automattic\BlocksEngine\PhpTransformer\Css\CssFontShorthand;
use Automattic\BlocksEngine\PhpTransformer\Css\CssLength;
use Automattic\BlocksEngine\PhpTransformer\Css\CssValueSplitter;
use DOMElement;
Expand Down
1 change: 1 addition & 0 deletions php-transformer/src/HtmlToBlocks/Style/StyleResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style;

use Automattic\BlocksEngine\PhpTransformer\Css\CssFontShorthand;
use Automattic\BlocksEngine\PhpTransformer\Css\CssIdent;
use Automattic\BlocksEngine\PhpTransformer\Css\CssPropertyName;
use Automattic\BlocksEngine\PhpTransformer\Css\CssSelectorMatcher;
Expand Down
19 changes: 18 additions & 1 deletion php-transformer/src/WordPressSitePlan/PresetBinding.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Automattic\BlocksEngine\PhpTransformer\Contract\CaptureEvidence;
use Automattic\BlocksEngine\PhpTransformer\Contract\ResponsiveCorrespondence;
use Automattic\BlocksEngine\PhpTransformer\Css\CssColor;
use Automattic\BlocksEngine\PhpTransformer\Css\CssFontShorthand;
use Automattic\BlocksEngine\PhpTransformer\Css\CssLength;
use Automattic\BlocksEngine\PhpTransformer\Css\CssSelectorMatcher;
use Automattic\BlocksEngine\PhpTransformer\Css\CssStylesheetTransformer;
Expand Down Expand Up @@ -938,7 +939,9 @@ private function bindBlock(string $name, string $json, string $markup, int $afte
* for a block literal.
* - A font size binds only when it is the preset's own length, and a
* font family only when it is the preset's own stack, so no fallback
* or rounding changes what renders.
* or rounding changes what renders. Font-shorthand custom properties
* bind only an exactly matching complete stack, in place, leaving the
* rest of the shorthand literal.
*/
public function bindStylesheet(string $css): string
{
Expand Down Expand Up @@ -1157,6 +1160,20 @@ private function bindDeclaration(string $declaration, bool $rootSelector = false
$value = trim(substr($value, 0, -strlen($match[0])));
}
if ('' === $value) return $declaration;
if (str_starts_with($property, '--')) {
$font = CssFontShorthand::expand($value, false, $familyOffset);
if (isset($font['font-family'])) {
$family = substr($value, $familyOffset);
$slug = $this->exactFontFamilySlug($family);
foreach ($this->system->fontFamilies as $preset) {
if (null !== $slug && $preset['slug'] === $slug && $this->sameFont($family, $preset['fontFamily'])) {
++$this->bound;
return self::sourcePresets($parts[0]) . ':' . self::sourcePresets(substr($value, 0, $familyOffset))
. 'var(--wp--preset--font-family--' . $slug . ')' . $important;
}
}
}
}
if ((str_starts_with($property, '--') || in_array($property, array('background', 'background-image'), true)) && null === CssColor::parse($this->resolve($value))) {
$bound = $this->bindColorTokens($value);
return $bound === self::sourcePresets($value) ? $declaration : self::sourcePresets($parts[0]) . ':' . $bound . $important;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);

require dirname(__DIR__, 2) . '/vendor/autoload.php';

use Automattic\BlocksEngine\PhpTransformer\ArtifactCompiler\ArtifactCompiler;

$assert = static function (bool $condition, string $message): void {
if (!$condition) throw new RuntimeException($message);
};

$stack = 'Example Light,Example,sans-serif';
$html = '<!doctype html><html><head><style>:root{--t:normal normal normal 18px/1.6em ' . $stack
. ';--spaced:italic 700 18px / 1.6em ' . $stack . '!important'
. ';--near:18px Example Light,serif;--unmatched:italic 700 21px / 1.4 Unknown Face,serif;--c:47,46,46;--sh:0 0 2px #2f2e2e}'
. '.copy{font:var(--t);color:rgb(var(--c))}</style></head><body><main>'
. str_repeat('<p class="copy">Body copy with a typography token.</p>', 6) . '</main></body></html>';
$plan = (new ArtifactCompiler())->compile(array('entrypoint' => 'index.html', 'files' => array('index.html' => $html)))->toArray()['source_reports']['wordpress_site_plan'];
$css = '';
$families = $palette = array();
foreach ($plan['writes'] as $write) {
if ('theme.json' === $write['target_path']) {
$theme = json_decode($write['payload']['data'], true, 512, JSON_THROW_ON_ERROR);
$families = array_column($theme['settings']['typography']['fontFamilies'], 'fontFamily', 'slug');
$palette = array_column($theme['settings']['color']['palette'], 'color', 'slug');
}
if (str_ends_with($write['target_path'], '.css')) $css .= $write['payload']['data'];
}
$slug = array_search($stack, $families, true);
$colorSlug = array_search('#2f2e2e', $palette, true);
$assert(is_string($slug), 'The rendered literal stack has an exactly matching preset.');
$assert(is_string($colorSlug), 'The rendered colour has an exactly matching preset.');
// Print the relevant emitted declarations as before/after evidence, even on failure.
preg_match_all('/--(?:t|unmatched):[^;}]+|color:var\(--wp--preset--color--[^;}]+/', $css, $definitions);
echo implode("\n", array_unique($definitions[0])) . "\n";
$assert(str_contains($css, '--unmatched:italic 700 21px / 1.4 Unknown Face,serif'), 'An unmatched shorthand stays literal.');
$assert(str_contains($css, '--near:18px Example Light,serif'), 'Sharing only a primary family is not an exact stack match.');
$assert(str_contains($css, '--spaced:italic 700 18px / 1.6em var(--wp--preset--font-family--' . $slug . ')!important'), 'Style, weight, size, line-height, spacing and priority survive family binding.');
$assert(str_contains($css, 'color:var(--wp--preset--color--' . $colorSlug . ',rgb(var(--c)))'), 'Colour variable binding retains its original fallback.');
$assert(str_contains($css, '--sh:0 0 2px var(--wp--preset--color--' . $colorSlug . ',#2f2e2e)'), 'A non-font custom property keeps its colour token binding.');
$assert(str_contains($css, '--t:normal normal normal 18px/1.6em var(--wp--preset--font-family--' . $slug . ')'), 'Only the matching shorthand family is bound in place.');
echo "Font shorthand custom property binding contract passed.\n";
10 changes: 9 additions & 1 deletion php-transformer/tests/unit/font-shorthand-resolved-cascade.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require dirname(__DIR__, 2) . '/vendor/autoload.php';

use Automattic\BlocksEngine\PhpTransformer\ArtifactCompiler\ArtifactCompiler;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style\CssFontShorthand;
use Automattic\BlocksEngine\PhpTransformer\Css\CssFontShorthand;
use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style\StyleResolver;

$assert = static function (bool $ok, string $message): void {
Expand Down Expand Up @@ -51,6 +51,14 @@
foreach (array('font-style' => 'italic', 'font-variant' => 'small-caps', 'font-weight' => 'bold', 'font-size' => '18px', 'line-height' => 'normal', 'font-family' => '"PT Sans",Arial,sans-serif') as $property => $value) {
$assert($value . ' !important' === ($expanded[$property] ?? null), 'expand ' . $property);
}
foreach (array(
' italic 700 18px / 1.6em "Example Light", Example, sans-serif',
'normal normal normal 18px/1.6em Example Light,Example,sans-serif',
) as $shorthand) {
$font = CssFontShorthand::expand($shorthand, false, $familyOffset);
$assert(isset($font['font-family']) && null !== $familyOffset, 'ordinary shorthands expose a family offset');
$assert(str_starts_with(substr($shorthand, $familyOffset), '"Example Light"') || str_starts_with(substr($shorthand, $familyOffset), 'Example Light'), 'family offset preserves the original prefix bytes');
}
// The same block is read by both authored projection and cascade consumers.
$resolver = (new ReflectionClass(StyleResolver::class))->newInstanceWithoutConstructor();
foreach (array('font:bold 18px Arial' => 'bold 18px Arial') as $style => $font) {
Expand Down
Loading