feat: lit/library-make-global – node to global module, all or nothing (E10)

- NewLibraryEntry: spec and creation of a new library entry, shared by
  lit/library-save and lit/library-make-global (NewDraft, P7, StoredNodeGuard)
- UsageMatcher::referencesAny() (ruling Q7): a global entry never names any
  entry in any spelling Divi renders (P23/P24 forms, Divi 4 shortcodes inside
  Divi 5 values, values casting to 0); used by library-save scope global and
  make-global
- make-global: entry created, node replaced by GlobalInstance::of, page through
  DiviWriter::commit; refused or rolled-back page deletes the entry, a partial
  page write keeps it (the page may show it); sources are pages and posts only

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-27 21:35:48 +02:00
co-authored by Claude Opus 5.5
parent 4f70886006
commit 44c1804c4c
9 changed files with 830 additions and 101 deletions
+33 -6
View File
@@ -17,6 +17,9 @@ defined('ABSPATH') || exit;
* newest layout (et_pb_load_global_module, WP_Query p=0), so it counts for EVERY entry;
* - an opener with JSON escapes (or a globalModule/global_module) that cannot be decoded, or content
* the regex engine cannot scan, counts as a possible reference.
* referencesAny() asks the same for every entry at once (ruling Q7): content that becomes a global entry
* must not render another entry – nor, through a value that casts to 0, the newest layout, which may be
* that entry itself.
* Pure PHP.
*/
final class UsageMatcher
@@ -34,7 +37,25 @@ final class UsageMatcher
public static function shows(string $content, int $id): bool
{
$textual = preg_match('/"globalModule":"0*' . $id . '"/', $content);
return self::matches($content, $id);
}
/**
* Whether $content renders any library entry, in every spelling shows() knows (ruling Q7): a
* globalModule key with any value, a global_module attribute Divi does not skip; fail closed.
*/
public static function referencesAny(string $content): bool
{
return self::matches($content, null);
}
/** shows() for entry $id, or for any entry when $id is null. */
private static function matches(string $content, ?int $id): bool
{
$textual = preg_match(
$id === null ? '/"globalModule"\s*:/' : '/"globalModule":"0*' . $id . '"/',
$content
);
if ($textual !== 0) {
return true; // A match, or a regex failure (fail closed).
}
@@ -73,8 +94,8 @@ final class UsageMatcher
return false;
}
/** Whether a Divi 4 shortcode in $text renders entry $id (or, casting to 0, any entry). */
private static function shortcodeShows(string $text, int $id): bool
/** Whether a Divi 4 shortcode in $text renders entry $id (null: any entry); a value casting to 0 renders any. */
private static function shortcodeShows(string $text, ?int $id): bool
{
if (stripos($text, 'global_module') === false) {
return false;
@@ -95,7 +116,7 @@ final class UsageMatcher
continue;
}
$cast = (int) $value;
if ($cast === 0 || $cast === $id) {
if ($id === null || $cast === 0 || $cast === $id) {
return true;
}
}
@@ -140,10 +161,16 @@ final class UsageMatcher
return $atts;
}
/** @param array<array-key, mixed> $attrs Whether a globalModule key at any depth names $id. */
private static function names(array $attrs, int $id): bool
/**
* @param array<array-key, mixed> $attrs Whether a globalModule key at any depth names $id (null: any
* globalModule key, whatever its value – fail closed).
*/
private static function names(array $attrs, ?int $id): bool
{
foreach ($attrs as $key => $value) {
if ($id === null && $key === 'globalModule') {
return true;
}
if (
$key === 'globalModule'
&& (is_int($value) || (is_string($value) && preg_match('/^\d+$/D', $value) === 1))