PluginProbe
ZIP AI – AI Website Builder & AI Agent / 0.0.11
ZIP AI – AI Website Builder & AI Agent v0.0.11
0.0.14 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 0.0.11, at inc/core/site-blueprint.php

135 lines 4.5 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 public static function register_contract(): void {
71 register_setting(
72 'general',
73 self::OPTION_KEY,
74 array(
75 'type' => 'string',
76 'show_in_rest' => false,
77 'default' => '',
78 )
79 );
80 }
81
82 /**
83 * Keep the blueprint out of the autoloaded set.
84 *
85 * Core only sizes an option off automatically above 150,000 bytes
86 * (`wp_max_autoloaded_option_size`) and a blueprint measures under that, so
87 * it would autoload by default. A filter rather than a one-time
88 * `add_option(..., false)`: the existence check that would need loads the
89 * whole payload on every request — the exact cost being avoided.
90 *
91 * @param bool|null $autoload The autoload value core is about to use.
92 * @param string $option The option being written.
93 * @return bool|null False for the blueprint, untouched for everything else.
94 */
95 public static function never_autoload( $autoload, $option ) {
96 return self::OPTION_KEY === $option ? false : $autoload;
97 }
98
99 /**
100 * The printable entries of a `rootAttrs` map (lowercase name, string value) — applied
101 * on write and again on print.
102 *
103 * @param mixed $attrs The plan's `rootAttrs`, or the stored option value.
104 * @return array<string,string>
105 */
106 public static function html_attrs_from( $attrs ): array {
107 if ( ! is_array( $attrs ) ) {
108 return array();
109 }
110 $row = array();
111 foreach ( $attrs as $name => $value ) {
112 if ( is_string( $name ) && is_string( $value ) && preg_match( '/^[a-z][a-z0-9-]*$/D', $name ) ) {
113 $row[ $name ] = $value;
114 }
115 }
116 return $row;
117 }
118
119 /**
120 * `language_attributes` filter: append the row to core's `lang`/`dir` output.
121 *
122 * @param string $output The attributes core assembled.
123 * @return string
124 */
125 public static function add_html_attrs( $output ) {
126 if ( is_admin() ) {
127 return $output;
128 }
129 foreach ( self::html_attrs_from( get_option( self::HTML_ATTRS_OPTION_KEY ) ) as $name => $value ) {
130 $output .= ' data-' . $name . '="' . esc_attr( $value ) . '"';
131 }
132 return $output;
133 }
134 }
135