wp/src/Shortcode/Shortcode.php

169 lines
5.2 KiB
PHP

<?php
namespace vlw\WP\Shortcode;
class Shortcode {
private readonly string $source;
protected readonly int $shortcode_open_offset_end;
protected readonly int $shortcode_close_offset_end;
protected readonly int $shortcode_open_offset_start;
protected readonly int $shortcode_close_offset_start;
/**
* Returns an array of all shortcodes by name in source
*
* @param string $source
* @param string $name
* @return array<static>
*/
public static function get_shortcodes(string $source, string $name): array {
$matches = [];
preg_match_all("/\[{$name} /", $source, $matches, PREG_OFFSET_CAPTURE);
return array_map(
fn(array $match): static => new static($source, $match[1]),
$matches[0]
);
}
/**
* Start parsing of a shortcode at a provided offset
*
* @param string $source
* @param int $start_offset
*/
public function __construct(string &$source, int $start_offset) {
$this->source = $source;
$this->shortcode_open_offset_start = $start_offset;
// The end of the opening portion of this shortcode will be at the next ] character
$this->shortcode_open_offset_end = strpos($this->source, "]", $this->shortcode_open_offset_start);
// Find and set the matching start and end offsets for the matching shortcode (there could be shortcode nesting)
$shortcode_close_offset = $this->resolve_shortcode_close_offsets();
$this->shortcode_close_offset_start = $this->shortcode_open_offset_end + $shortcode_close_offset->start;
$this->shortcode_close_offset_end = $this->shortcode_open_offset_end + $shortcode_close_offset->end;
}
/**
* Returns the name of this shortcode
*
* @return string
*/
final public string $name {
get {
$first_space_offset = strpos($this->shortcode_open_contents(), " ");
return substr($this->shortcode_open_contents(), 0, $first_space_offset);
}
}
/**
* Returns the content of this shortcode
*
* @return string
*/
final public string $content {
get {
$length = ($this->shortcode_close_offset_start - $this->shortcode_open_offset_end) - 1;
return substr($this->source, $this->shortcode_open_offset_end + 1, $length);
}
}
/**
* Get the contents of an attribute on this shortcode by attribute name
*
* @param string $name
* @return string|false Returns false if an attribute with $name can not be found
*/
public function attribute(string $name): string|false {
$value = null;
$attr_offset_start = strpos($this->shortcode_open_contents(), $name);
// Bail out if the target attribute can't be found
if ($attr_offset_start === false) {
return false;
}
// Truncate the shortcode open tag content to the start of the target attribute
$shortcode_attr_start = substr($this->shortcode_open_contents(), $attr_offset_start);
// Find the first space and equals character in the remaining string after the target attribute
$first_space_offset = (int) strpos($shortcode_attr_start, " ");
$first_equals_offset = (int) strpos($shortcode_attr_start, "=");
// We found an equals character before a space, this is an attribute with a defined value
if ($first_equals_offset < $first_space_offset) {
// Resolve the character used to wrap the attribute value (probably quotes or double quotes)
$attr_value_bounding_char = substr($shortcode_attr_start, $first_equals_offset + 1, 1);
// The value is found after the upcoming =" characters
$value_offset_start = $first_equals_offset + 2;
// Find the end of the value by looking for the next char that is used to wrap the value
$value_offset_end = strpos($shortcode_attr_start, $attr_value_bounding_char, $value_offset_start);
// The length of the value will be between the first and last wrapper characters
$length = $value_offset_end - $value_offset_start;
$value = substr($shortcode_attr_start, $value_offset_start, $length);
}
return $value ?? "";
}
/**
* Get the content of the shortcode open tag
*
* @return string
*/
private function shortcode_open_contents(): string {
$length = ($this->shortcode_open_offset_end - $this->shortcode_open_offset_start) - 1;
return substr($this->source, $this->shortcode_open_offset_start + 1, $length);
}
/**
* Attempt to find the correct matching closing shortcode tag offsets for this shortcode
*
* @return object
*/
private function resolve_shortcode_close_offsets(): object {
$depth = 0;
$matches = [];
$offset_source = substr($this->source, $this->shortcode_open_offset_end);
preg_match_all("/\[/", $offset_source, $matches, PREG_OFFSET_CAPTURE);
// Find the matching closing shortcode for the current shortcode
foreach ($matches[0] as $match) {
[$char, $offset_start] = $match;
$next_char = substr($offset_source, $offset_start + 1, 1);
// This is a closing shortcode, decrement the tag depth
if ($next_char === "/") {
--$depth;
}
// Break out of the loop if we found the matching closing shortcode
if ($depth < 0) {
break;
}
// This is an opening shortcode, increase the current tag depth and continue
$depth++;
}
$offset_end = strpos($offset_source, "]", $offset_start);
return (object) [
"end" => $offset_end,
"start" => $offset_start
];
}
}