*/ 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 ]; } }