apply_to_catalog( $content, $design );
}
if ( 'single-academy_courses' === $slug ) {
return $this->apply_to_course_page( $content, $design );
}
return $content;
}
/**
* Replace the card in a course grid, and set its columns.
*
* @param string $content Template content.
* @param array $design Design.
* @return string
*/
private function apply_to_catalog( $content, array $design ) {
$blocks = parse_blocks( $content );
$columns = (int) $design['catalog']['columns']['desktop'];
$card = $this->card_blocks( $design );
$this->walk(
$blocks,
function ( &$block ) use ( $card, $columns ) {
if ( 'core/post-template' !== $block['blockName'] ) {
return;
}
$block['attrs']['layout'] = [
'type' => 'grid',
'columnCount' => $columns,
];
$this->replace_inner( $block, $card );
}
);
return $this->serialize( $blocks );
}
/**
* Rebuild a course page from the design: section order, what is switched
* off, and which side the enroll box is on.
*
* @param string $content Template content.
* @param array $design Design.
* @return string
*/
private function apply_to_course_page( $content, array $design ) {
$blocks = parse_blocks( $content );
$sections = $this->section_blocks( $design );
$this->walk(
$blocks,
function ( &$block ) use ( $sections, $design ) {
if ( 'core/columns' !== $block['blockName'] || count( $block['innerBlocks'] ) < 2 ) {
return;
}
$main = $block['innerBlocks'][0];
$sidebar = $block['innerBlocks'][1];
$this->replace_inner( $main, $sections );
if ( 'none' === $design['single']['sidebar'] ) {
$main['attrs']['width'] = '100%';
$this->replace_inner( $block, [ $main ] );
return;
}
$columns = 'left' === $design['single']['sidebar'] ? [ $sidebar, $main ] : [ $main, $sidebar ];
$this->replace_inner( $block, $columns );
}
);
return $this->serialize( $blocks );
}
/**
* The blocks of a course card, in the design's order.
*
* @param array $design Design.
* @return array Parsed blocks.
*/
public function card_blocks( array $design ) {
$pattern = Patterns::post( 'card' );
if ( $pattern ) {
return Patterns::block( $pattern );
}
return parse_blocks( $this->card_markup( $design ) );
}
/**
* The markup of a course card, in the design's order.
*
* @param array $design Design.
* @return string
*/
public function card_markup( array $design ) {
$radius = (int) $design['style']['radius'];
$markup = [
// These sizes are still baked in here (the value at the moment this
// pattern is (re)generated) — Gutenberg's style engine mangles a raw
// var(--x) reference used as a border/typography JSON attribute
// value into invalid content (confirmed: it corrupts "--" into the
// literal text "u002d", which then fails the block's own save()
// round-trip and shows "Block contains unexpected or invalid
// content" for every nested block). Actually following Shape
// changes after the fact is handled instead by the plain CSS
// override in tokens() below, which never touches block attributes.
'image' => sprintf(
'',
esc_attr( $design['catalog']['imageRatio'] ),
max( 0, $radius - 1 )
),
'badge' => '',
'categories' => '',
// No font size here on purpose: the Shape title size is a CSS
// default in tokens() instead, so moving that slider reaches cards
// already on a page and a size set on this block still wins.
'title' => '',
'excerpt' => '',
'instructors' => '',
'meta' => '',
'rating' => '',
'price' => '',
'button' => '',
'wishlist' => '',
];
$visible = [];
foreach ( $design['catalog']['elements'] as $element ) {
if ( ! empty( $element['visible'] ) && isset( $markup[ $element['key'] ] ) ) {
$visible[] = $element['key'];
}
}
$image = '';
if ( $visible && 'image' === $visible[0] ) {
$image = $markup['image'];
array_shift( $visible );
}
// Price, rating and the button share one row at the foot of the card
// when they are the last parts; anywhere else they follow the order.
$row = [ 'rating', 'price', 'button', 'wishlist' ];
$footer = '';
while ( $visible && in_array( end( $visible ), $row, true ) ) {
$footer = $markup[ array_pop( $visible ) ] . $footer;
}
$body = '';
foreach ( $visible as $key ) {
if ( 'image' !== $key ) {
$body .= $markup[ $key ];
}
}
if ( '' !== $footer ) {
$footer = ''
. '
' . $footer . '
'
. '';
}
$card = sprintf(
''
. ''
. '%2$s'
. ''
. '
%3$s%4$s
'
. ''
. '
'
. '',
max( 0, $radius ),
$image,
$body,
$footer
);
return $card;
}
/**
* The blocks of a course page's main column, in the design's order.
*
* @param array $design Design.
* @return array Parsed blocks.
*/
public function section_blocks( array $design ) {
$pattern = Patterns::post( 'page' );
if ( $pattern ) {
return Patterns::block( $pattern );
}
return parse_blocks( $this->section_markup( $design ) );
}
/**
* The markup of a course page's main column, in the design's order.
*
* @param array $design Design.
* @return string
*/
public function section_markup( array $design ) {
$markup = [
'media' => '',
'categories' => '',
'title' => '',
'meta' => ''
. ''
. ''
. ''
. ''
. '
',
'description' => '',
'benefits' => '',
'curriculum' => '',
'attachments' => '',
'instructorProfile' => '',
'ratingSummary' => '',
'reviewForm' => '',
'reviews' => '',
];
$content = '';
foreach ( $design['single']['sections'] as $section ) {
if ( ! empty( $section['visible'] ) && isset( $markup[ $section['key'] ] ) ) {
$content .= $markup[ $section['key'] ];
}
}
return $content;
}
/**
* Colours and sizes from the design, as CSS.
*
* @param array $design Design.
* @return string
*/
public function tokens( array $design ) {
$style = $design['style'];
$css = '';
// Colours come from Academy's palette (Customize → Colours & Style), which
// prints for every page; the design only adds shape.
$css .= '--academy-card-radius:' . (int) $style['radius'] . 'px;';
$css .= '--academy-button-radius:' . (int) $style['buttonRadius'] . 'px;';
$columns = $design['catalog']['columns'];
$grid = sprintf(
'@media(max-width:1024px){.academy-course-archive .wp-block-post-template{grid-template-columns:repeat(%1$d,minmax(0,1fr))!important}}'
. '@media(max-width:600px){.academy-course-archive .wp-block-post-template{grid-template-columns:repeat(%2$d,minmax(0,1fr))!important}}',
(int) $columns['tablet'],
(int) $columns['mobile']
);
// The card pattern bakes its radius in as plain px at the moment it is
// (re)generated (see card_markup()) rather than as a var(--x) block
// attribute, because Gutenberg's style engine mangles that into invalid
// block content. A later Shape change reaches already-generated cards
// through this override instead.
//
// The title size is not baked in, so it needs no !important. It is a
// plain two-class rule: that beats the theme's own heading / Post Title
// styles (element or :root :where() rules), so the Shape slider sets the
// card title size, while a size set on that Post Title block in the
// editor still wins — a preset class carries !important and a custom
// value is an inline style. (Wrapped in :where() it had no specificity
// and the theme's h3 size won, so the slider did nothing.)
$shape = sprintf(
'.academy-course-card{border-radius:%1$dpx!important}'
. '.academy-course-card .wp-block-post-featured-image img{border-radius:%2$dpx %2$dpx 0 0!important}'
. '.academy-course-card .wp-block-post-title{font-size:%3$dpx}',
max( 0, (int) $style['radius'] ),
max( 0, (int) $style['radius'] - 1 ),
max( 0, (int) $style['titleSize'] )
);
return ':root{' . $css . '}' . $grid . $shape . $this->classic_shape( $style );
}
/**
* Card corners, button corners and title size for the classic course cards
* (`.academy-course`, drawn by PHP templates). Block cards carry these in
* their own markup, so without this the Shape settings do nothing on a site
* that still uses the classic templates.
*
* Only once the design has been saved (or is being previewed): the design's
* defaults are not the classic card's own look, and a site that never
* touched Customize should not change on update.
*
* @param array $style Design style.
* @return string
*/
private function classic_shape( array $style ) {
if ( '__none__' === get_option( Settings::OPTION, '__none__' ) ) {
return '';
}
// `:root body` outweighs the card's own nested rules without !important.
return sprintf(
':root body .academy-course{border-radius:%1$dpx;overflow:hidden}'
. ':root body .academy-course .academy-course__title{font-size:%2$dpx}'
. ':root body .academy-course .academy-btn{border-radius:%3$dpx}',
(int) $style['radius'],
(int) $style['titleSize'],
(int) $style['buttonRadius']
);
}
/**
* Print the design's CSS on the front end.
*
* @return void
*/
public function print_tokens() {
// Settings::sanitize() constrains every token to a value primitive
// (hex colors, numeric sizes, enum keywords). wp_strip_all_tags adds a
// belt-and-braces guarantee that no stray `` or `