<?php
$balanced_string_regex = "~(?x) # Free-Spacing
(?(DEFINE) # Define a few subroutines
(?<double>“(?:(?!&[lr]squo;).)*”) # full set of doubles (no quotes inside)
(?<single>‘(?:(?!&[lr]dquo;).)*’) # full set of singles (no quotes inside)
(?<notquotes>(?:(?!&[lr][sd]quo;).)*) # chars that are not quotes
) # end DEFINE
^ # Start of string
(?: # Start non-capture group
(?¬quotes) # Any non-quote chars
&l(?<type>[sd])quo; # Opening quote, capture single or double type
# any full singles, doubles, not quotes or recursion
(?:(?&single)|(?&double)|(?¬quotes)|(?R))*
&r\k<type>quo; # Closing quote of the correct type
(?¬quotes) #
)++ # Repeat non-capture group
$ # End of string
~";
$string = "“He said ” ‘He said ’";
check_string($string);
$string = "<p>“Wait a moment,” Jacey said. The street light lit up his aged, rat face. Who’s on the move?”</p>";
check_string($string);
$string = "<p>“Wait a moment,” Jacey said. The street light lit up his aged, rat face. ‘Whos on the “move?” ’</p>";
check_string($string);
$string = "<p>“He said he’ coming afer you,” Harry said, and he’ bringing the boys too!”</p>";
check_string($string);
$string = "<p>“He ‘said he’ coming afer you,” Harry said, and he“ bringing the boys too!”</p>";
check_string($string);
function check_string($string) {
global $balanced_string_regex;
echo (preg_match($balanced_string_regex, $string)) ?
"Balanced!\n" :
" Nah... Not Balanced.\n" ;
}
PD9waHAKJGJhbGFuY2VkX3N0cmluZ19yZWdleCA9ICJ+KD94KSAgICAgICAgICAgICAgICAgICMgRnJlZS1TcGFjaW5nCig/KERFRklORSkgICAgICAgICAgICAjIERlZmluZSBhIGZldyBzdWJyb3V0aW5lcwogICAoPzxkb3VibGU+JmxkcXVvOyg/Oig/ISZbbHJdc3F1bzspLikqJnJkcXVvOykgICMgZnVsbCBzZXQgb2YgZG91YmxlcyAobm8gcXVvdGVzIGluc2lkZSkKICAgKD88c2luZ2xlPiZsc3F1bzsoPzooPyEmW2xyXWRxdW87KS4pKiZyc3F1bzspICAjIGZ1bGwgc2V0IG9mIHNpbmdsZXMgKG5vIHF1b3RlcyBpbnNpZGUpCiAgICg/PG5vdHF1b3Rlcz4oPzooPyEmW2xyXVtzZF1xdW87KS4pKikgICAgICAgICAgIyBjaGFycyB0aGF0IGFyZSBub3QgcXVvdGVzCikgICAgICAgICAgICAgICAgICAgICAjIGVuZCBERUZJTkUKCl4gICAgICAgICAgICAgICAgICAgICAgICMgU3RhcnQgb2Ygc3RyaW5nCig/OiAgICAgICAgICAgICAgICAgICAgICMgU3RhcnQgbm9uLWNhcHR1cmUgZ3JvdXAKICAgKD8mbm90cXVvdGVzKSAgICAgICAgIyBBbnkgbm9uLXF1b3RlIGNoYXJzCiAgICZsKD88dHlwZT5bc2RdKXF1bzsgICMgT3BlbmluZyBxdW90ZSwgY2FwdHVyZSBzaW5nbGUgb3IgZG91YmxlIHR5cGUKICAgIyBhbnkgZnVsbCBzaW5nbGVzLCBkb3VibGVzLCBub3QgcXVvdGVzIG9yIHJlY3Vyc2lvbgogICAoPzooPyZzaW5nbGUpfCg/JmRvdWJsZSl8KD8mbm90cXVvdGVzKXwoP1IpKSoKICAgJnJcazx0eXBlPnF1bzsgICAgICAgIyBDbG9zaW5nIHF1b3RlIG9mIHRoZSBjb3JyZWN0IHR5cGUKICAgKD8mbm90cXVvdGVzKSAgICAgICMgCikrKyAgICAgICAgICAgICAgICAgICAjIFJlcGVhdCBub24tY2FwdHVyZSBncm91cAokICAgICAgICAgICAgICAgICAgICAgIyBFbmQgb2Ygc3RyaW5nCn4iOwoKJHN0cmluZyA9ICImbGRxdW87SGUgc2FpZCAgJnJkcXVvOyAmbHNxdW87SGUgc2FpZCAgJnJzcXVvOyI7CmNoZWNrX3N0cmluZygkc3RyaW5nKTsKJHN0cmluZyA9ICI8cD4mbGRxdW87V2FpdCBhIG1vbWVudCwmcmRxdW87IEphY2V5IHNhaWQuIFRoZSBzdHJlZXQgbGlnaHQgbGl0IHVwIGhpcyBhZ2VkLCByYXQgZmFjZS4gV2hvJnJzcXVvO3Mgb24gdGhlIG1vdmU/JnJkcXVvOzwvcD4iOwpjaGVja19zdHJpbmcoJHN0cmluZyk7CiRzdHJpbmcgPSAiPHA+JmxkcXVvO1dhaXQgYSBtb21lbnQsJnJkcXVvOyBKYWNleSBzYWlkLiBUaGUgc3RyZWV0IGxpZ2h0IGxpdCB1cCBoaXMgYWdlZCwgcmF0IGZhY2UuICZsc3F1bztXaG9zIG9uIHRoZSAmbGRxdW87bW92ZT8mcmRxdW87ICZyc3F1bzs8L3A+IjsKY2hlY2tfc3RyaW5nKCRzdHJpbmcpOwokc3RyaW5nID0gIjxwPiZsZHF1bztIZSBzYWlkIGhlJnJzcXVvOyBjb21pbmcgYWZlciB5b3UsJnJkcXVvOyBIYXJyeSBzYWlkLCBhbmQgaGUmcnNxdW87IGJyaW5naW5nIHRoZSBib3lzIHRvbyEmcmRxdW87PC9wPiI7CmNoZWNrX3N0cmluZygkc3RyaW5nKTsKJHN0cmluZyA9ICI8cD4mbGRxdW87SGUgJmxzcXVvO3NhaWQgaGUmcnNxdW87IGNvbWluZyBhZmVyIHlvdSwmcmRxdW87IEhhcnJ5IHNhaWQsIGFuZCBoZSZsZHF1bzsgYnJpbmdpbmcgdGhlIGJveXMgdG9vISZyZHF1bzs8L3A+IjsKY2hlY2tfc3RyaW5nKCRzdHJpbmcpOwoKCmZ1bmN0aW9uIGNoZWNrX3N0cmluZygkc3RyaW5nKSB7CglnbG9iYWwgJGJhbGFuY2VkX3N0cmluZ19yZWdleDsKCWVjaG8gKHByZWdfbWF0Y2goJGJhbGFuY2VkX3N0cmluZ19yZWdleCwgJHN0cmluZykpID8KICAgICAgICAiQmFsYW5jZWQhXG4iIDoKCQkiIE5haC4uLiBOb3QgQmFsYW5jZWQuXG4iIDsKfQo=