PluginProbe
ZIP AI – AI Website Builder & AI Agent / trunk
ZIP AI – AI Website Builder & AI Agent vtrunk
0.0.13 0.0.12 0.0.11 0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / inc / core / site-blueprint.php

site-blueprint.php in ZIP AI – AI Website Builder & AI Agent trunk, at inc/core/site-blueprint.php

138 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Site Blueprint — the plan the site was built from, stored WITH the site.
4 *
5 * The builder plans a whole website once: its sitemap, style, design tokens,
6 * and the exact header/footer bytes every page wears. That plan lived only in
7 * the build session, so a page added later — days later, from a fresh session —
8 * had nothing to build against and re-derived its own design. Same site, two
9 * looks. Storing the plan here is what lets "add a page" and vibe-editing's
10 * "add a section" build into the family the site already has.
11 *
12 * The plan is reached ONLY through two brain-only abilities, `zipai/get-site-plan`
13 * and `zipai/update-site-plan` (visibility `internal`, so the AI chat catalog never
14 * lists them). It is deliberately NOT exposed to REST: a plan is ~200 KB, and
15 * `/wp/v2/settings` serves every registered option whole, ignoring `_fields` when
16 * dispatched through `rest_do_request` — so a plan in that collection turned every
17 * settings read into a truncated response, the model's reads for a site title
18 * included. `zipai/run-wp-cli` is no door either: it caps a command at 4,096 bytes.
19 *
20 * One slice IS printed: `rootAttrs`, the lever row (`data-style`, `data-motion`, …)
21 * the site's JS and root-headed CSS read. `zipai/update-site-plan` copies it into the
22 * small autoloaded `zip-ai-site-html-attrs` option; `language_attributes` prints it.
23 *
24 * @since 0.0.9
25 * @package zip-ai
26 */
27
28 namespace ZipAI\MCP\Classes\Core;
29
30 if ( ! defined( 'ABSPATH' ) ) {
31 exit;
32 }
33
34 /**
35 * Registers the site-blueprint option — see the file header.
36 */
37 class Site_Blueprint {
38
39 /**
40 * The stored plan, as the JSON the builder produced.
41 *
42 * @var string
43 */
44 const OPTION_KEY = 'zip-ai-site-blueprint';
45
46 /**
47 * The plan's `rootAttrs`, `name => value`, printed on `<html>` as `data-<name>="<value>"`.
48 *
49 * @var string
50 */
51 const HTML_ATTRS_OPTION_KEY = 'zip-ai-site-html-attrs';
52
53 /**
54 * Hook registration.
55 */
56 public static function init(): void {
57 add_action( 'init', array( __CLASS__, 'register_contract' ) );
58 add_filter( 'wp_default_autoload_value', array( __CLASS__, 'never_autoload' ), 10, 2 );
59 add_filter( 'language_attributes', array( __CLASS__, 'add_html_attrs' ) );
60 }
61
62 /**
63 * Register the option for its type and default only — never for REST.
64 *
65 * No sanitize_callback: the value is a JSON document, and WP's string
66 * sanitizers would corrupt it. It is written only by the authenticated
67 * builder over MCP and is never echoed into a page; its `rootAttrs` are
68 * copied to HTML_ATTRS_OPTION_KEY, the one option that IS printed on `<html>`.
69 *
70 * The group is the plugin's own, which no admin form posts: wp-admin/options.php
71 * writes null to every option of the posted group the form did not send.
72 */
73 public static function register_contract(): void {
74 register_setting(
75 'zip_ai',
76 self::OPTION_KEY,
77 array(
78 'type' => 'string',
79 'show_in_rest' => false,
80 'default' => '',
81 )
82 );
83 }
84
85 /**
86 * Keep the blueprint out of the autoloaded set.
87 *
88 * Core only sizes an option off automatically above 150,000 bytes
89 * (`wp_max_autoloaded_option_size`) and a blueprint measures under that, so
90 * it would autoload by default. A filter rather than a one-time
91 * `add_option(..., false)`: the existence check that would need loads the
92 * whole payload on every request — the exact cost being avoided.
93 *
94 * @param bool|null $autoload The autoload value core is about to use.
95 * @param string $option The option being written.
96 * @return bool|null False for the blueprint, untouched for everything else.
97 */
98 public static function never_autoload( $autoload, $option ) {
99 return self::OPTION_KEY === $option ? false : $autoload;
100 }
101
102 /**
103 * The printable entries of a `rootAttrs` map (lowercase name, string value) — applied
104 * on write and again on print.
105 *
106 * @param mixed $attrs The plan's `rootAttrs`, or the stored option value.
107 * @return array<string,string>
108 */
109 public static function html_attrs_from( $attrs ): array {
110 if ( ! is_array( $attrs ) ) {
111 return array();
112 }
113 $row = array();
114 foreach ( $attrs as $name => $value ) {
115 if ( is_string( $name ) && is_string( $value ) && preg_match( '/^[a-z][a-z0-9-]*$/D', $name ) ) {
116 $row[ $name ] = $value;
117 }
118 }
119 return $row;
120 }
121
122 /**
123 * `language_attributes` filter: append the row to core's `lang`/`dir` output.
124 *
125 * @param string $output The attributes core assembled.
126 * @return string
127 */
128 public static function add_html_attrs( $output ) {
129 if ( is_admin() ) {
130 return $output;
131 }
132 foreach ( self::html_attrs_from( get_option( self::HTML_ATTRS_OPTION_KEY ) ) as $name => $value ) {
133 $output .= ' data-' . $name . '="' . esc_attr( $value ) . '"';
134 }
135 return $output;
136 }
137 }
138