tag there and nothing * on the page ran (17 Oct 2026, the customer page of any set with rules, * where the stylesheet had already gone out earlier in the request). While a * hold is on, both helpers keep their blocks here instead, and whoever * started it puts them on the page as tags of their own. * * @param bool $on True starts a hold; false ends it. * @return array{css:string[],js:string[]} On ending a hold, what it held. */ function addon_boss_inline_hold( bool $on ): array { $held = &addon_boss_inline_held(); $empty = array( 'css' => array(), 'js' => array(), ); if ( $on ) { $held = $empty; return $empty; } $out = is_array( $held ) ? $held : $empty; $held = null; return $out; } /** * The blocks held so far, by reference: null while no hold is on. * * @return array|null */ function &addon_boss_inline_held() { static $held = null; return $held; } /** * The same for a block of CSS. Late styles print with the footer, so a rule * added while the page body renders still arrives. */ function addon_boss_inline_css( string $css ): void { $css = trim( $css ); if ( '' === $css ) { return; } $held = &addon_boss_inline_held(); if ( is_array( $held ) ) { $held['css'][] = $css; return; } $handle = 'addon-boss-inline'; if ( wp_doing_ajax() || wp_style_is( $handle, 'done' ) || did_action( 'wp_print_footer_scripts' ) || did_action( 'admin_print_footer_scripts' ) ) { // Core never grew a wp_print_inline_style_tag() to match the script one, // so the late path hands the CSS to core anyway rather than building a // tag itself: a throwaway src-less handle carrying the block as inline // data, printed on the spot. Core writes the tag, so this function has // no raw output anywhere in it and needs no escaping exemption. // // A fresh handle each time, because a printed handle is 'done' and core // will not print it twice; two late blocks on one page would otherwise // lose the second. static $bb_late = 0; ++$bb_late; $one_shot = $handle . '-late-' . $bb_late; wp_register_style( $one_shot, false, array(), ADDON_BOSS_VERSION ); wp_add_inline_style( $one_shot, $css ); wp_print_styles( $one_shot ); return; } if ( ! wp_style_is( $handle, 'registered' ) ) { wp_register_style( $handle, false, array(), ADDON_BOSS_VERSION ); } wp_enqueue_style( $handle ); wp_add_inline_style( $handle, $css ); } /** * What this build of the engine can do. * * An extension point. The engine asks what it can DO, and the answer comes from * the modules that are installed: each one registers the capabilities it * implements on the addon_boss_engine_capabilities filter. A capability nothing * registers is something this build has no code for, the same way it has no * code for anything else nobody has written. * * Anything can answer this filter, including another plugin. Register a * capability string and the engine will offer whatever your module implements. */ function addon_boss_engine_can( string $capability ): bool { return in_array( $capability, (array) apply_filters( 'addon_boss_engine_capabilities', array() ), true ); } /** * Is this field type offered by this build? * * The two heavy types ride with the module that implements them: a file field * needs the upload machinery, a measured size field the measurement maths. * Everything else (text, number, dropdown, tick box, section label, info block) * is part of the engine itself and always offered. * * A stored field of an unavailable type is inert rather than deleted: never * rendered, priced or required, its saved config untouched, so it returns whole * the moment the module is installed. */ function addon_boss_field_type_available( string $type ): bool { if ( 'file' === $type ) { return addon_boss_engine_can( 'uploads' ); } if ( 'size' === $type ) { return addon_boss_engine_can( 'measured' ); } return true; } /** * The field types a merchant may pick from, in the words they are offered in. * * ONE list, for both places that ask: the "Add a field" bar and the type picker * inside a field's own row. They were written separately and drifted apart twice * over: the add bar hid the types this build has no code for while the row * picker offered them all, and the add bar printed each type's menu wording * ("Date, with lead time and rush charges") while the row picker printed the bare label * ("Date"), so the same control had two names depending on where you met it. * * @param string $keep A type to include even when this build does not offer it. * The row picker passes the field's OWN stored type, because * dropping it would leave the row showing somebody else's * type and quietly change the field on the next save. * @return array Type key to the words a merchant reads. */ function addon_boss_field_type_menu( string $keep = '' ): array { $out = array(); foreach ( addon_boss_personalisation_field_types() as $type => $def ) { $type = (string) $type; if ( ! addon_boss_field_type_available( $type ) && $type !== $keep ) { continue; } // A type's menu wording is what it is offered as; the label is its // shorter name for lists and columns. Menu first, so the merchant reads // the same words wherever the type is met. $out[ $type ] = (string) ( $def['menu'] ?? $def['label'] ?? $type ); } return $out; } /** * Is this fee mode offered by this build? * * Per-letter, percentage and per-value pricing ride with the fee-modes module; * a flat fee, charged once or for each, is the engine's own. A field priced by * an unavailable mode keeps its input and charges nothing, and the merchant's * stored fee is left untouched. */ function addon_boss_fee_mode_available( string $mode ): bool { return in_array( $mode, array( 'char', 'percent', 'per_value' ), true ) ? addon_boss_engine_can( 'fee-modes' ) : true; } /** * Which product types can carry option sets on their own page. * * A simple product is the engine's own surface. A variable parent rides with the * module that reconciles two choice systems on one page (its variations and its * sets), so without that module a variable parent offers none. * * @return string[] */ function addon_boss_options_product_types(): array { $types = array( 'simple' ); if ( addon_boss_engine_can( 'variable-options' ) ) { $types[] = 'variable'; } return $types; } // Engine modules - the addon_boss_* primitives, lifted from BundleBoss at the // split. Required via __DIR__ so the engine always loads its own siblings. require_once __DIR__ . '/swatches.php'; require_once __DIR__ . '/pricing.php'; require_once __DIR__ . '/variations.php'; // The standalone options engine: option sets, product fields, the fee and value // maths, the resolvers, virtual variations and the grid split. This is the // engine's own surface and a normal file, present in every build. require_once __DIR__ . '/standalone.php'; // The option set preview panel on the set editor: the draft form rendered // as shoppers will see it, through the same sanitiser and renderer the real // save and the real page use. Nothing is persisted by previewing. require_once __DIR__ . '/set-preview.php'; // The set editor's frame: labelled sections, one Save bar in reach, and the // customer's page beside the editor. Layout only; the form and its save // are the set types' own (AddOnBoss #122). require_once __DIR__ . '/editor-frame.php'; // Conditional reveal: the show/hide rules on sets and fields. The basic // vocabulary (is, is not, is any of, answered, empty) runs in every build; // the numeric compares ask the 'reveal' capability and stand down to // "simply shows" without it. require_once __DIR__ . '/reveal.php'; // Reading a variable product into option sets, and building the option version // beside it as a draft. Present in every build: the merchant who needs it most // is the one who has not paid yet. What it cannot carry (a SKU per pair, and // stock per pair without the plan) it says out loud before it writes anything. // What a pair weighs and measures, and the pair key both maps are stored // under. Free: wrong postage costs a merchant money on every order. require_once __DIR__ . '/combo-shipping.php'; require_once __DIR__ . '/variation-convert.php'; // Optional engine modules. // // Anything in pro/ is loaded, in name order. Each one defines its own functions // and registers its own hooks, so a directory with nothing in it simply means // the engine has no code for those things, the same as any other feature it was // never written to do. There is nothing to switch on or off here and nothing to // ask: a module either exists on disk or it does not. // // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- transient file-scope path holders, unset immediately below. $bb_engine_modules = glob( __DIR__ . '/pro/*.php' ); if ( is_array( $bb_engine_modules ) ) { sort( $bb_engine_modules ); foreach ( $bb_engine_modules as $bb_engine_module ) { require_once $bb_engine_module; } } unset( $bb_engine_modules, $bb_engine_module ); // phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound // --- more engine modules land here as Phase A proceeds ---