<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Shortcodes inside Aura's sections.
 *
 * Why they were needed: a section does NOT go through WordPress's pipeline. `the_content` applies
 * `do_shortcode` at priority 11 to the SAVED content, while we return our own HTML at priority 99
 * — long after that train has gone (and we cannot run at a low priority: wpautop would tear our
 * <style> and <script> apart, see {@see AIPB_Render::init}). The result: a shortcode written into
 * a section reached the page exactly as typed, printed as text. Hence this class: WE do the
 * expansion, on our own HTML, once the section is finished.
 *
 * Three choices worth having written down:
 *
 *  1. IT EXPANDS ON THE FINISHED SECTION, after the CSS has been scoped and the scripts
 *     neutralised (see {@see AIPB_Render::light_wrap}). What a shortcode produces belongs to the
 *     plugin that registered it: its <style> and <script> have to stay as that plugin wrote them —
 *     scoping them under the section's id, or running them against our runtime's proxied
 *     document, breaks sliders, maps and forms that expect the real document.
 *
 *  2. NO EXPANSION INSIDE <script> AND <style>. WordPress only recognises registered tags, but
 *     among them are ones that are also ordinary words (`video`, `audio`, `gallery`, `embed`): a
 *     `data[video]` in a piece of JavaScript would be replaced with a player. The section's JS and
 *     CSS are skipped up front.
 *
 *  3. IN THE SNAPSHOT THEY STAY LITERAL (see {@see AIPB_Render::compile_html}). The snapshot in
 *     post_content is static and exists for SEO plugins, previews and the "plugin deactivated"
 *     case: freezing the contact form into it — nonce included — would be a mistake. Staying
 *     literal, on the other hand, means whoever scans post_content to decide whether to enqueue
 *     their own assets (has_shortcode(), which is what CF7 and friends do) FINDS them: that is how
 *     the plugin's CSS reaches the page even though its shortcode lives in our meta.
 */
class AIPB_Shortcodes {

	/**
	 * Expands the shortcodes in an already-rendered section's HTML.
	 *
	 * The `aipb_do_shortcodes` filter switches expansion off, globally or case by case.
	 */
	public static function expand( $html ) {
		$html = (string) $html;

		if ( '' === $html || false === strpos( $html, '[' ) || ! self::any_registered() ) {
			return $html;
		}
		if ( ! apply_filters( 'aipb_do_shortcodes', true, $html ) ) {
			return $html;
		}

		// A net for the rude shortcodes, the ones that PRINT instead of returning: in a page their
		// output would land at the top of the document (annoying), and in the preview — which is a
		// JSON REST response — it would break it, and the section would disappear entirely. We
		// capture it and put it back after the section.
		ob_start();

		// The section's JS and CSS are left untouched: only what sits between them is expanded.
		$out  = '';
		$last = 0;

		if ( preg_match_all( '#<(script|style)\b[^>]*>.*?</\1\s*>#is', $html, $matches, PREG_OFFSET_CAPTURE ) ) {
			foreach ( $matches[0] as $match ) {
				$out .= do_shortcode( substr( $html, $last, $match[1] - $last ) );
				$out .= $match[0];
				$last = $match[1] + strlen( $match[0] );
			}
		}

		$out .= do_shortcode( substr( $html, $last ) );

		return $out . ob_get_clean();
	}

	/**
	 * Does the value have the SHAPE of a single self-closing shortcode — `[tag attributes]` and
	 * nothing else?
	 *
	 * Deliberately narrow: no surrounding text, no enclosing form ([tag]…[/tag]), no `<` or `>`
	 * inside. Field values come from the panel and are not markup: what passes here is shortcode
	 * syntax, not a way to slip HTML into a section.
	 */
	public static function is_bare( $value ) {
		$value = trim( (string) $value );

		return '' !== $value && 1 === preg_match( '/^\[[a-zA-Z0-9_-]+[^\]\[<>]*\]$/', $value );
	}

	/** Like is_bare(), but the tag has to be REALLY registered on this site. */
	public static function is_value( $value ) {
		if ( ! self::is_bare( $value ) ) {
			return false;
		}

		preg_match( '/^\[([a-zA-Z0-9_-]+)/', trim( (string) $value ), $m );

		return shortcode_exists( $m[1] );
	}

	/**
	 * The site's "real" shortcodes: the ones plugins and the theme put there.
	 *
	 * The AI needs them (see {@see AIPB_Site_Context}): without knowing them the model invents
	 * `[contact_form]` and the page ends up with `[contact_form]` written on it. Left out are the
	 * core's, which make no sense in a section's markup — galleries and media are written in HTML —
	 * and our own, which nobody pastes by hand.
	 */
	public static function registered() {
		global $shortcode_tags;

		if ( ! self::any_registered() ) {
			return array();
		}

		$skip = (array) apply_filters( 'aipb_context_exclude_shortcodes', array(
			'wp_caption', 'caption', 'gallery', 'playlist', 'audio', 'video', 'embed',
			'ai_section',
		) );

		$tags = array_values( array_diff( array_keys( $shortcode_tags ), $skip ) );
		sort( $tags );

		return $tags;
	}

	/** Is there at least one shortcode registered on this site? */
	protected static function any_registered() {
		global $shortcode_tags;

		return ! empty( $shortcode_tags ) && is_array( $shortcode_tags );
	}
}
