diff --git a/composer.json b/composer.json index de1d559..f644798 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,8 @@ "require": { "php": "^7.2.5|^8.0|^8.1", "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0", - "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0" + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0", + "illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0" }, "require-dev": { "orchestra/testbench": "~3.8.0|^4.0|^5.0|^6.3|^7.0|^8.0", diff --git a/src/Compiler.php b/src/Compiler.php index 06a9518..567c5bf 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -93,9 +93,9 @@ protected static function shortcodeRegex(string $tag): string public static function resolveAttributes(string $attributesText): ?array { $attributesText = preg_replace("/[\x{00a0}\x{200b}]+/u", ' ', $attributesText); - + $attributes = collect([]); - + if (preg_match_all(static::attributeRegex(), $attributesText, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { if (! empty($match[1])) { @@ -112,13 +112,13 @@ public static function resolveAttributes(string $attributesText): ?array $attributes[] = stripcslashes($match[9]); } } - + // Reject any unclosed HTML elements. $filteredAttributes = $attributes->filter(function ($attribute) { if (strpos($attribute, '<') === false) { return true; } - + return preg_match('/^[^<]*+(?:<[^>]*+>[^<]*+)*+$/', $attribute); }); @@ -141,6 +141,6 @@ public static function resolveAttributes(string $attributesText): ?array */ protected static function attributeRegex(): string { - return '/([\w-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|\'([^\']*)\'(?:\s|$)|(\S+)(?:\s|$)/'; + return '/([\w.:-]+)\s*=\s*"([^"]*)"(?:\s|$)|([\w.:-]+)\s*=\s*\'([^\']*)\'(?:\s|$)|([\w.:-]+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|\'([^\']*)\'(?:\s|$)|(\S+)(?:\s|$)/'; } } diff --git a/src/Shortcode.php b/src/Shortcode.php index a790cec..e92dd9e 100644 --- a/src/Shortcode.php +++ b/src/Shortcode.php @@ -4,9 +4,12 @@ use Illuminate\Support\Str; use Illuminate\Support\Collection; +use Illuminate\Database\Eloquent\Concerns\HasAttributes; abstract class Shortcode { + use HasAttributes; + /** * The cache of a list of Shortcode classes. * @@ -29,11 +32,11 @@ abstract class Shortcode protected $tag; /** - * The shortcode's attributes. + * Optional closing tag to match in content. * - * @var array|null + * @var string */ - protected $attributes = null; + protected $closingTag; /** * The shortcode's body content. @@ -48,7 +51,7 @@ abstract class Shortcode * @param array|null $attributes * @param string|null $body */ - public function __construct(array $attributes = null, string $body = null) + public function __construct(array $attributes = [], string $body = null) { $this->attributes = $attributes; @@ -103,13 +106,56 @@ public function dispatch(array $matches): ?string } // Set up our inputs and run our handle. - $this->attributes = Compiler::resolveAttributes($attributes); + $this->attributes = $this->setAttributeDefaults( + Compiler::resolveAttributes($attributes) + ); $this->body = $body; return $this->handle(); } + /** + * Set defaults for attributes without a value that should be cast to boolean. + * e.g. [shortcode boolean-attribute string="value"] + * + * @return void + */ + protected function setAttributeDefaults($attributes) + { + $attributes = [ + ...$this->getCastAttributeDefaults(), + ...collect($attributes ?? []) + ->mapWithKeys(function ($value, $key) { + if (array_key_exists($value, $this->casts) && in_array($this->casts[$value], ['boolean', 'bool'])) { + return [$value => true]; + } + + return [$key => $value]; + }) + ->toArray() + ]; + + return empty($attributes)? null : $attributes; + } + + /** + * Retrieve the default values for attributes that should be cast to boolean. + * + * @return array + */ + public function getCastAttributeDefaults(): array + { + return collect($this->casts) + ->filter(function($value){ + return in_array($value, ['boolean', 'bool']); + }) + ->map(function(){ + return false; + }) + ->toArray(); + } + /** * Retrieve all of the Shortcode classes. * @@ -198,4 +244,62 @@ public static function compile(string $content, Collection $shortcodes = null): { return Compiler::compile($content, $shortcodes); } + + /** + * Get the attributes that should be converted to dates. + * + * @return array + */ + public function getDates() + { + return []; + } + + /** + * Get the casts array. + * + * @return array + */ + public function getCasts() + { + return $this->casts; + } + + /** + * Get an attribute from the class. + * + * @param string $key + * @return mixed + */ + public function getAttribute($key) + { + if (! $key) { + return; + } + + $key = $key === Str::camel($key) && !array_key_exists($key, $this->casts) + ? Str::snake($key, '-') + : $key; + + // If the attribute exists in the attribute array or has a "get" mutator we will + // get the attribute's value + if (array_key_exists($key, $this->attributes ?? []) || + array_key_exists($key, $this->casts) || + $this->hasGetMutator($key)) { + return $this->getAttributeValue($key); + } + + return null; + } + + /** + * Dynamically retrieve attributes on the class. + * + * @param string $key + * @return mixed + */ + public function __get($key) + { + return $this->getAttribute($key); + } } diff --git a/tests/ShortcodeTest.php b/tests/ShortcodeTest.php index dffb402..368910e 100644 --- a/tests/ShortcodeTest.php +++ b/tests/ShortcodeTest.php @@ -4,7 +4,12 @@ use tehwave\Shortcodes\Compiler; use tehwave\Shortcodes\Shortcode; +use Illuminate\Support\Facades\Date; +use tehwave\Shortcodes\Tests\Shortcodes\CastDate; +use tehwave\Shortcodes\Tests\Shortcodes\CastFloat; use tehwave\Shortcodes\Tests\Shortcodes\OutputBody; +use tehwave\Shortcodes\Tests\Shortcodes\CastBoolean; +use tehwave\Shortcodes\Tests\Shortcodes\CastInteger; use tehwave\Shortcodes\Tests\Shortcodes\OutputAttributes; class ShortcodeTest extends TestCase @@ -28,6 +33,10 @@ protected function setUp(): void $this->shortcodes = collect([ new OutputBody, new OutputAttributes, + new CastBoolean, + new CastDate, + new CastFloat, + new CastInteger, ]); } @@ -135,4 +144,35 @@ public function testShortcodeAttributesSyntaxes(): void $this->assertSame($expected, $compiledContent); }); } + + /** + * Test the various castings for attributes. + * + * @link https://unit-tests.svn.wordpress.org/trunk/tests/shortcode.php + * + * @return void + */ + public function testShortcodeAttributesCasting(): void + { + collect([ + '[cast_boolean test-boolean]' => 'true', + // '[cast_boolean test-boolean="true"]' => 'true', + '[cast_boolean test-boolean="1"]' => 'true', + // '[cast_boolean test-boolean="false"]' => 'false', + '[cast_boolean test-boolean="0"]' => 'false', + '[cast_boolean /]' => 'false', + '[cast_date test-date="2023-06-29"]' => (string) Date::parse('2023-06-29')->timestamp, + '[cast_date test-date="2020-01-01"]' => (string) Date::parse('2020-01-01')->timestamp, + '[cast_integer test-int="3"]' => '6', + '[cast_integer test-int="35460"]' => '70920', + '[cast_float test-float="5.67"]' => '15.67', + '[cast_float test-float="15.011"]' => '25.011', + ])->each(function ($output, $tag) { + $compiledContent = Compiler::compile($tag, $this->shortcodes); + + $expected = $output; + + $this->assertSame($expected, $compiledContent); + }); + } } diff --git a/tests/Shortcodes/CastBoolean.php b/tests/Shortcodes/CastBoolean.php new file mode 100644 index 0000000..3f30cfc --- /dev/null +++ b/tests/Shortcodes/CastBoolean.php @@ -0,0 +1,25 @@ + 'boolean', + ]; + + /** + * The code to run when the Shortcode is being compiled. + * + * You may return a string from here, that will then + * be inserted into the content being compiled. + * + * @return string|null + */ + public function handle(): ?string + { + return $this->testBoolean? 'true' : 'false'; + } +} diff --git a/tests/Shortcodes/CastDate.php b/tests/Shortcodes/CastDate.php new file mode 100644 index 0000000..c69581f --- /dev/null +++ b/tests/Shortcodes/CastDate.php @@ -0,0 +1,25 @@ + 'date', + ]; + + /** + * The code to run when the Shortcode is being compiled. + * + * You may return a string from here, that will then + * be inserted into the content being compiled. + * + * @return string|null + */ + public function handle(): ?string + { + return (string) $this->testDate->timestamp; + } +} diff --git a/tests/Shortcodes/CastFloat.php b/tests/Shortcodes/CastFloat.php new file mode 100644 index 0000000..0defda9 --- /dev/null +++ b/tests/Shortcodes/CastFloat.php @@ -0,0 +1,25 @@ + 'float', + ]; + + /** + * The code to run when the Shortcode is being compiled. + * + * You may return a string from here, that will then + * be inserted into the content being compiled. + * + * @return string|null + */ + public function handle(): ?string + { + return (string)($this->testFloat + 10); + } +} diff --git a/tests/Shortcodes/CastInteger.php b/tests/Shortcodes/CastInteger.php new file mode 100644 index 0000000..e1ba92e --- /dev/null +++ b/tests/Shortcodes/CastInteger.php @@ -0,0 +1,25 @@ + 'integer', + ]; + + /** + * The code to run when the Shortcode is being compiled. + * + * You may return a string from here, that will then + * be inserted into the content being compiled. + * + * @return string|null + */ + public function handle(): ?string + { + return (string)($this->testInt * 2); + } +}