1313
1414use function json_decode ;
1515use function json_encode ;
16+ use function json_last_error ;
1617use function preg_quote ;
1718use function preg_replace ;
1819use function preg_replace_callback ;
1920use function sprintf ;
2021use function str_contains ;
2122use function substr ;
2223
24+ use function trim ;
25+
26+ use const JSON_ERROR_NONE ;
27+ use const PHP_VERSION_ID ;
28+
2329class HeadlessUserInt
2430{
2531 public const STANDARD = 'HEADLESS_INT ' ;
2632 public const NESTED = 'NESTED_HEADLESS_INT ' ;
2733 public const STANDARD_NULLABLE = 'HEADLESS_INT_NULL ' ;
2834 public const NESTED_NULLABLE = 'NESTED_HEADLESS_INT_NULL ' ;
29- private const REGEX = '/("|)%s_START<<(.*?)>>%s_END("|)/s ' ;
35+
36+ private const REGEX = '/(?P<quote> \\\\"|")?(?P<type>%s|%s)_START<<(?P<content>(?:[^>]|>(?!>(?P=type)_END))*+)>>(?P=type)_END(?P=quote)?/sS ' ;
37+
38+ /** @var array<string, string> */
39+ private static array $ regexPatterns = [];
3040
3141 public function wrap (string $ content , string $ type = self ::STANDARD ): string
3242 {
3343 return preg_replace (
3444 '/( ' . preg_quote ('<!--INT_SCRIPT. ' , '/ ' ) . '[0-9a-z]{32} ' . preg_quote ('--> ' , '/ ' ) . ')/ ' ,
3545 sprintf ('%s_START<<\1>>%s_END ' , $ type , $ type ),
3646 $ content
37- );
47+ ) ?? $ content ;
3848 }
3949
4050 public function hasNonCacheableContent (string $ content ): bool
@@ -46,67 +56,80 @@ public function unwrap(string $content): string
4656 {
4757 if (str_contains ($ content , self ::NESTED )) {
4858 $ content = preg_replace_callback (
49- sprintf (self ::REGEX , self ::NESTED , self ::NESTED ),
50- [$ this , 'replace ' ],
51- $ content
52- );
53- }
54-
55- if (str_contains ($ content , self ::NESTED_NULLABLE )) {
56- $ content = preg_replace_callback (
57- sprintf (self ::REGEX , self ::NESTED_NULLABLE , self ::NESTED_NULLABLE ),
58- function (array $ content ) {
59- return $ this ->replace ($ content , true );
60- },
59+ $ this ->buildPattern (self ::NESTED , self ::NESTED_NULLABLE ),
60+ fn (array $ m ) => $ this ->replace ($ m , $ m ['type ' ] === self ::NESTED_NULLABLE ),
6161 $ content
62- );
63- }
64-
65- if (str_contains ($ content , self ::STANDARD_NULLABLE )) {
66- $ content = preg_replace_callback (
67- sprintf (self ::REGEX , self ::STANDARD_NULLABLE , self ::STANDARD_NULLABLE ),
68- function (array $ content ) {
69- return $ this ->replace ($ content , true );
70- },
71- $ content
72- );
62+ ) ?? $ content ;
7363 }
7464
7565 return preg_replace_callback (
76- sprintf (self ::REGEX , self :: STANDARD , self ::STANDARD ),
77- [ $ this , ' replace ' ] ,
66+ $ this -> buildPattern (self ::STANDARD , self ::STANDARD_NULLABLE ),
67+ fn ( array $ m ) => $ this -> replace ( $ m , $ m [ ' type ' ] === self :: STANDARD_NULLABLE ) ,
7868 $ content
69+ ) ?? $ content ;
70+ }
71+
72+ protected function buildPattern (string $ primary , string $ nullable ): string
73+ {
74+ return self ::$ regexPatterns [$ primary ] ??= sprintf (
75+ self ::REGEX ,
76+ preg_quote ($ nullable , '/ ' ),
77+ preg_quote ($ primary , '/ ' )
7978 );
8079 }
8180
82- /**
83- * for use in preg_replace_callback
84- * to unwrap all HEADLESS_INT<<>>HEADLESS_INT blocks
85- *
86- * @param array<int, string> $input
87- */
88- private function replace (array $ input , bool $ returnNull = false ): ?string
81+ protected function replace (array $ m , bool $ isNullable ): string
8982 {
90- $ content = $ input [2 ];
91- if ($ input [1 ] === $ input [3 ] && $ input [1 ] === '" ' ) {
92- // have a look inside if it might be json already
93- $ decoded = json_decode ($ content );
83+ $ hasQuotes = $ m ['quote ' ] !== '' ;
84+ $ rawContent = (string )$ m ['content ' ];
9485
95- if (empty ($ decoded ) && $ returnNull ) {
96- return json_encode (null );
86+ if ($ hasQuotes ) {
87+ if ($ this ->isJson ($ rawContent )) {
88+ return $ rawContent ;
89+ }
90+
91+ $ decoded = json_decode ($ rawContent );
92+
93+ if (empty ($ decoded ) && $ isNullable ) {
94+ return 'null ' ;
9795 }
9896
9997 if ($ decoded !== null ) {
100- return $ content ;
98+ return $ rawContent ;
10199 }
102- return json_encode ($ content );
100+
101+ return json_encode ($ rawContent );
102+ }
103+
104+ $ jsonEncoded = json_encode ($ rawContent );
105+
106+ if ($ jsonEncoded !== false && $ jsonEncoded [0 ] === '" ' ) {
107+ return substr ($ jsonEncoded , 1 , -1 );
108+ }
109+
110+ return $ jsonEncoded ?: '' ;
111+ }
112+
113+ protected function isJson (string $ string ): bool
114+ {
115+ $ string = trim ($ string );
116+
117+ if ($ string === '' ) {
118+ return false ;
103119 }
104120
105- // trim one occurrence of double quotes at both ends
106- $ jsonEncoded = json_encode ($ content );
107- if ($ jsonEncoded [0 ] === '" ' && $ jsonEncoded [-1 ] === '" ' ) {
108- $ jsonEncoded = substr ($ jsonEncoded , 1 , -1 );
121+ $ first = $ string [0 ];
122+ $ last = $ string [-1 ];
123+
124+ if (!(($ first === '{ ' && $ last === '} ' ) || ($ first === '[ ' && $ last === '] ' ))) {
125+ return false ;
126+ }
127+
128+ if (PHP_VERSION_ID >= 80300 ) {
129+ return json_validate ($ string );
109130 }
110- return $ jsonEncoded ;
131+
132+ json_decode ($ string );
133+ return json_last_error () === JSON_ERROR_NONE ;
111134 }
112135}
0 commit comments