mirror of
https://codeberg.org/vlw/wp.git
synced 2026-02-26 03:51:58 +01:00
169 lines
5.2 KiB
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
|
|
];
|
|
}
|
|
}
|