get_field_value( $field_id );
if ( null === $value || '' === $value ) {
return;
}
echo slbuild_escape( (string) $value, $context ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- slbuild_escape() is the escaping function
}
/**
* Return a section field value, escaped for the given context.
*
* @param string $field_id
* @param string $context
* @return string
*/
function slbuild_get_field( string $field_id, string $context = 'html' ): string {
$value = SLBUILD_SectionLoader::instance()->get_field_value( $field_id );
if ( null === $value || '' === $value ) {
return '';
}
return slbuild_escape( (string) $value, $context );
}
/**
* Output a richtext field value, passed through wp_kses_post.
* Only for richtext fields — not for plain text.
*
* @param string $field_id
*/
function slbuild_raw( string $field_id ): void {
$value = SLBUILD_SectionLoader::instance()->get_field_value( $field_id );
if ( null === $value || '' === $value ) {
return;
}
echo wp_kses_post( (string) $value );
}
/**
* Output a block field value inside a block loop, escaped for context.
*
* @param string $field_id
* @param string $context
*/
function slbuild_block_field( string $field_id, string $context = 'html' ): void {
$value = SLBUILD_SectionLoader::instance()->get_block_field_value( $field_id );
if ( null === $value || '' === $value ) {
return;
}
echo slbuild_escape( (string) $value, $context ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- slbuild_escape() is the escaping function
}
/**
* Return a block field value inside a block loop, escaped for context.
*
* @param string $field_id
* @param string $context
* @return string
*/
function slbuild_get_block_field( string $field_id, string $context = 'html' ): string {
$value = SLBUILD_SectionLoader::instance()->get_block_field_value( $field_id );
if ( null === $value || '' === $value ) {
return '';
}
return slbuild_escape( (string) $value, $context );
}
/**
* Get the blocks array for the current section instance.
* Use in a foreach loop inside a section template.
*
* @return array
*/
function slbuild_get_blocks(): array {
return SLBUILD_SectionLoader::instance()->get_blocks();
}
/**
* Render all sections for the current page (or given post).
* Call this from the theme template where sections should appear.
*
* @param int|null $post_id
*/
function slbuild_render_page( $post_id = null ): void {
SLBUILD_SectionLoader::instance()->render_page( $post_id );
}
/**
* Render all section instances for a global region (header, footer, etc.).
*
* @param string $region Region slug — matches the saved wp_options key, e.g. 'header', 'footer'.
*/
function slbuild_render_global_region( string $region ): void {
SLBUILD_SectionLoader::instance()->render_global_region( $region );
}
/**
* Read a single field value from a global region's saved instance data.
*
* Use this in functions.php, WC template overrides, or any PHP that needs
* a global setting without rendering a section — e.g. free shipping threshold,
* font choice, shop column count.
*
* @param string $region Region slug, e.g. 'theme-settings'.
* @param string $field_id Field ID from the section schema, e.g. 'free_shipping_threshold'.
* @param mixed $default Fallback value if the field is not saved.
* @return mixed The saved field value, or $default.
*/
function slbuild_global_setting( string $region, string $field_id, $default = '' ) {
static $cache = array();
if ( ! isset( $cache[ $region ] ) ) {
$store = new SLBUILD_DataStore();
$raw = $store->get_global( $region );
if ( is_array( $raw ) && ! empty( $raw['layout'] ) && ! empty( $raw['instances'] ) ) {
// Grab settings from the first instance in the layout.
$first = $raw['layout'][0] ?? null;
$inst_id = $first['instance'] ?? '';
$cache[ $region ] = $inst_id && isset( $raw['instances'][ $inst_id ]['settings'] )
? $raw['instances'][ $inst_id ]['settings']
: array();
} else {
$cache[ $region ] = array();
}
}
$val = $cache[ $region ][ $field_id ] ?? null;
return ( $val !== null && $val !== '' ) ? $val : $default;
}
/**
* Saved colour overrides per scheme, from the theme-settings global region.
* Only valid hex colours survive; anything else is dropped.
*
* @return array Map of scheme => key => hex colour.
*/
function slbuild_scheme_overrides(): array {
static $overrides = null;
if ( null !== $overrides ) {
return $overrides;
}
$overrides = array();
if ( ! function_exists( 'slbuild_global_setting' ) ) {
return $overrides;
}
foreach ( array( 'white', 'light', 'dark', 'primary', 'ink', 'custom' ) as $scheme ) {
foreach ( array( 'bg', 'text', 'sub', 'eyebrow', 'btn', 'btn2', 'card', 'border' ) as $key ) {
$hex = sanitize_hex_color( (string) slbuild_global_setting( 'theme-settings', $scheme . '_' . $key, '' ) );
if ( $hex ) {
$overrides[ $scheme ][ $key ] = $hex;
}
}
}
return $overrides;
}
/**
* CSS for the saved colour overrides. Each override becomes one fixed class,
* slbuild-ov-{scheme}-{key}, so the stylesheet never depends on runtime-built
* utility class names.
*
* @return string
*/
function slbuild_scheme_override_css(): string {
$css = '';
foreach ( slbuild_scheme_overrides() as $scheme => $keys ) {
foreach ( $keys as $key => $hex ) {
$sel = '.slbuild-ov-' . $scheme . '-' . $key;
if ( 'bg' === $key || 'card' === $key ) {
$css .= $sel . '{background-color:' . $hex . '}';
} elseif ( 'border' === $key ) {
$css .= $sel . '{border-color:' . $hex . '}';
} elseif ( 'btn' === $key ) {
$css .= $sel . '{background-color:' . $hex . ';color:' . slbuild_contrast_color( $hex ) . '}' . $sel . ':hover{filter:brightness(.92)}';
} elseif ( 'btn2' === $key ) {
$css .= $sel . '{border:1px solid ' . $hex . ';color:' . $hex . '}' . $sel . ':hover{background-color:' . $hex . ';color:' . slbuild_contrast_color( $hex ) . '}';
} else {
$css .= $sel . '{color:' . $hex . '}';
}
}
}
return $css;
}
/**
* Black or white, whichever reads better on the given hex background.
*
* @param string $hex A value already checked by sanitize_hex_color().
* @return string
*/
function slbuild_contrast_color( string $hex ): string {
$hex = ltrim( $hex, '#' );
if ( 3 === strlen( $hex ) ) {
$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
}
$luma = ( 0.299 * hexdec( substr( $hex, 0, 2 ) ) + 0.587 * hexdec( substr( $hex, 2, 2 ) ) + 0.114 * hexdec( substr( $hex, 4, 2 ) ) ) / 255;
return $luma > 0.6 ? '#1d1d1f' : '#ffffff';
}
/**
* Return Tailwind class strings for a named color scheme.
* Keys: bg, text, sub, eyebrow, btn, btn2, card, border.
*
* @param string $scheme One of: white, light, dark, primary, ink.
* @return array
*/
function slbuild_get_color_scheme( string $scheme = 'white' ): array {
static $defaults = null;
static $saved = null;
if ( null === $defaults ) {
$defaults = array(
'white' => array(
'bg' => 'bg-white',
'text' => 'text-[#1d1d1f]',
'sub' => 'text-[#86868b]',
'eyebrow' => 'text-[#0071e3]',
'btn' => 'bg-[#0071e3] hover:bg-[#0077ed] text-white',
'btn2' => 'border border-[#0071e3] text-[#0071e3] hover:bg-[#0071e3] hover:text-white',
'card' => 'bg-[#f5f5f7]',
'border' => 'border-[#d2d2d7]',
),
'light' => array(
'bg' => 'bg-[#f5f5f7]',
'text' => 'text-[#1d1d1f]',
'sub' => 'text-[#86868b]',
'eyebrow' => 'text-[#0071e3]',
'btn' => 'bg-[#0071e3] hover:bg-[#0077ed] text-white',
'btn2' => 'border border-[#0071e3] text-[#0071e3] hover:bg-[#0071e3] hover:text-white',
'card' => 'bg-white',
'border' => 'border-[#d2d2d7]',
),
'dark' => array(
'bg' => 'bg-[#1d1d1f]',
'text' => 'text-white',
'sub' => 'text-[#ababab]',
'eyebrow' => 'text-[#ababab]',
'btn' => 'bg-white hover:bg-[#f5f5f7] text-[#1d1d1f]',
'btn2' => 'border border-white text-white hover:bg-white hover:text-[#1d1d1f]',
'card' => 'bg-[#2c2c2e]',
'border' => 'border-[#3a3a3c]',
),
'primary' => array(
'bg' => 'bg-[#0071e3]',
'text' => 'text-white',
'sub' => 'text-white/70',
'eyebrow' => 'text-white/70',
'btn' => 'bg-white hover:bg-[#f5f5f7] text-[#0071e3]',
'btn2' => 'border border-white text-white hover:bg-white hover:text-[#0071e3]',
'card' => 'bg-[#0064cc]',
'border' => 'border-white/20',
),
'ink' => array(
'bg' => 'bg-[#2c2c2e]',
'text' => 'text-white',
'sub' => 'text-[#ababab]',
'eyebrow' => 'text-[#ababab]',
'btn' => 'bg-[#0071e3] hover:bg-[#0077ed] text-white',
'btn2' => 'border border-white text-white hover:bg-white hover:text-[#1d1d1f]',
'card' => 'bg-[#3a3a3c]',
'border' => 'border-[#48484a]',
),
'custom' => array(
'bg' => 'bg-white',
'text' => 'text-[#1d1d1f]',
'sub' => 'text-[#86868b]',
'eyebrow' => 'text-[#0071e3]',
'btn' => 'bg-[#0071e3] hover:bg-[#0077ed] text-white',
'btn2' => 'border border-[#0071e3] text-[#0071e3] hover:bg-[#0071e3] hover:text-white',
'card' => 'bg-[#f5f5f7]',
'border' => 'border-[#d2d2d7]',
),
);
}
// Merge saved scheme overrides from the theme-settings global region.
// An override is a plain hex colour; it is styled by a generated rule (see
// slbuild_scheme_override_css()), so only the class name is needed here.
if ( null === $saved ) {
$saved = array();
foreach ( slbuild_scheme_overrides() as $s => $keys ) {
foreach ( array_keys( $keys ) as $key ) {
$saved[ $s ][ $key ] = 'slbuild-ov-' . $s . '-' . $key;
}
}
}
$base = $defaults[ $scheme ] ?? $defaults['white'];
return isset( $saved[ $scheme ] ) ? array_merge( $base, $saved[ $scheme ] ) : $base;
}
/**
* Read a schema-level metadata key for the currently-rendering section.
* Reads top-level keys like heading_level, above_fold — not instance field values.
*
* @param string $key Schema key to read.
* @return mixed|null
*/
function slbuild_meta( string $key ) {
$schema = SLBUILD_SectionLoader::instance()->get_schema();
return $schema[ $key ] ?? null;
}
/**
* Output a complete
tag for an image field.
* Defaults to loading="lazy"; switches to loading="eager" when the section
* schema declares "above_fold": true (hero / first-visible sections).
* Width and height are added when the attachment ID can be resolved from the URL.
*
* @param string $field_id Image field id.
* @param string $alt_field_id Companion alt-text field id (empty = no alt).
* @param string $loading Force 'lazy' or 'eager'; empty = auto from schema.
*/
function slbuild_image( string $field_id, string $alt_field_id = '', string $loading = '' ): void {
$loader = SLBUILD_SectionLoader::instance();
$url = $loader->get_field_value( $field_id );
if ( ! $url ) {
return;
}
$url = (string) $url;
if ( '' === $loading ) {
$schema = $loader->get_schema();
$loading = ! empty( $schema['above_fold'] ) ? 'eager' : 'lazy';
}
$alt = '';
if ( '' !== $alt_field_id ) {
$alt_val = $loader->get_field_value( $alt_field_id );
$alt = $alt_val ? (string) $alt_val : '';
}
$width = 0;
$height = 0;
$attach_id = attachment_url_to_postid( $url );
if ( $attach_id ) {
$src = wp_get_attachment_image_src( $attach_id, 'full' );
if ( $src ) {
$width = (int) $src[1];
$height = (int) $src[2];
}
}
$attr = 'src="' . esc_url( $url ) . '"';
$attr .= ' alt="' . esc_attr( $alt ) . '"';
if ( $width ) $attr .= ' width="' . $width . '"';
if ( $height ) $attr .= ' height="' . $height . '"';
$attr .= ' loading="' . esc_attr( $loading ) . '"';
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '
';
}
/**
* Emit schema.org Product JSON-LD for a single product.
* Call once per product inside a product-grid section template.
*
* @param WC_Product $product
*/
function slbuild_product_jsonld( $product ): void {
if ( ! ( $product instanceof WC_Product ) ) {
return;
}
$data = array(
'@context' => 'https://schema.org',
'@type' => 'Product',
'name' => $product->get_name(),
'image' => wp_get_attachment_url( $product->get_image_id() ) ?: '',
'url' => get_permalink( $product->get_id() ),
'offers' => array(
'@type' => 'Offer',
'price' => $product->get_price(),
'priceCurrency' => get_woocommerce_currency(),
'availability' => $product->is_in_stock()
? 'https://schema.org/InStock'
: 'https://schema.org/OutOfStock',
),
);
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '' . "\n";
}
/**
* Escape a value for the given output context.
*
* @param string $value
* @param string $context 'html', 'attr', 'url', 'js', 'css_var'
* @return string
*/
function slbuild_escape( string $value, string $context ): string {
switch ( $context ) {
case 'attr':
return esc_attr( $value );
case 'url':
return esc_url( $value );
case 'js':
return esc_js( $value );
case 'css_var':
// Safe for use inside a CSS custom property value — strips anything
// that could break out of the declaration or inject a script tag.
return preg_replace( '/[^a-zA-Z0-9#%.\-_()\/ ]/', '', $value );
case 'html':
default:
return esc_html( $value );
}
}