<?php

if (!defined('ABSPATH')) {
	exit;
}

/*
 * Front-end TOC parsing + rendering helpers.
 * (Moved out of the former monolithic TOC file for maintainability.)
 */

if (!function_exists('anchorkit_toc_generate_anchor')) {
	/**
	 * Generate a URL-friendly anchor ID from a heading‟s text.
	 * Respects anchor format and custom prefix settings.
	 *
	 * @param string $text Heading text.
	 * @return string Normalised anchor slug with optional prefix.
	 */
	function anchorkit_toc_generate_anchor($text)
	{
		$runtime_settings = anchorkit_get_runtime_anchor_settings();

		if ($runtime_settings && isset($runtime_settings['anchor_format'])) {
			$anchor_format = sanitize_key($runtime_settings['anchor_format']);
		} else {
			$anchor_format = anchorkit_get_option('anchorkit_toc_anchor_format', 'auto');
		}
		if (!in_array($anchor_format, array('auto', 'sequential', 'prefixed'), true)) {
			$anchor_format = 'auto';
		}

		if ($runtime_settings && array_key_exists('anchor_prefix', $runtime_settings)) {
			$custom_prefix = sanitize_title_with_dashes($runtime_settings['anchor_prefix']);
		} else {
			$custom_prefix = sanitize_title_with_dashes(anchorkit_get_option('anchorkit_toc_anchor_prefix', 'section'));
		}

		// Sanitize the text for use as an anchor
		// Use the same sanitization logic as Gutenberg for consistency
		// Gutenberg removes underscores, while sanitize_title preserves them
		$sanitized = sanitize_title($text);

		// IMPORTANT: Match Gutenberg's ID generation exactly
		// Gutenberg removes underscores from IDs, but sanitize_title preserves them
		// So we need to remove underscores manually to match Gutenberg behavior
		$sanitized = str_replace('_', '', $sanitized);

		// Apply anchor format (values must match UI: auto | sequential | prefixed)
		switch ($anchor_format) {
			case 'prefixed':
				// Custom prefix + heading text
				if (!empty($custom_prefix)) {
					$anchor = $custom_prefix . '-' . $sanitized;
				} else {
					$anchor = $sanitized;
				}
				break;
			case 'sequential':
				// Base for sequential anchors; numeric suffix added by caller
				$anchor = 'toc';
				break;
			case 'auto':
			default:
				// Just the heading text (default WordPress behavior)
				$anchor = $sanitized;
				break;
		}

		/**
		 * Filter the generated anchor ID for a heading.
		 *
		 * @since 1.0.0
		 * @param string $anchor The generated anchor ID.
		 * @param string $text   The original heading text.
		 * @param string $format The anchor format (auto|sequential|prefixed).
		 */
		return apply_filters('anchorkit_anchor_id', $anchor, $text, $anchor_format);
	}
}

if (!function_exists('anchorkit_toc_parse_headings')) {
	/**
	 * Scan the provided HTML for headings and ensure they carry IDs.
	 * Returns the (possibly) modified HTML plus a collected array of headings.
	 *
	 * @param string   $content             HTML to scan (passed by reference so we can inject IDs).
	 * @param string[] $include_headings  Array of tag names (e.g. ['h2','h3']).
	 * @param string   $exclude_selectors  Comma-separated list of simple class/ID selectors to skip.
	 * @return array{headings:array,content:string}
	 */
	function anchorkit_toc_parse_headings(&$content, $include_headings = array('h2', 'h3', 'h4'), $exclude_selectors = '')
	{
		$headings = array();

		if (empty($include_headings)) {
			return array(
				'headings' => $headings,
				'content' => $content,
			);
		}

		// PRO: Get advanced filtering options - free version defaults
		$exclude_keywords = '';
		$exclude_patterns = '';
		$min_heading_depth = 2;
		$max_heading_depth = 4;

		// Premium feature overrides - this entire block is stripped from free version
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$exclude_keywords = anchorkit_get_option('anchorkit_toc_exclude_keywords', '');
				$exclude_patterns = anchorkit_get_option('anchorkit_toc_exclude_regex', '');
				$min_heading_depth = intval(anchorkit_get_option('anchorkit_toc_min_heading_depth', 2));
				$max_heading_depth = intval(anchorkit_get_option('anchorkit_toc_max_heading_depth', 4));
			}
		}

		// Check if H5 or H6 are explicitly included in the parameter (e.g., from Gutenberg block)
		// If so, we need to adjust the max depth filter to allow them
		$has_h5_or_h6 = in_array('h5', $include_headings, true) || in_array('h6', $include_headings, true);

		// If H5 or H6 are explicitly requested, ensure max_heading_depth allows them
		if ($has_h5_or_h6) {
			if (in_array('h6', $include_headings, true)) {
				$max_heading_depth = max($max_heading_depth, 6);
			} elseif (in_array('h5', $include_headings, true)) {
				$max_heading_depth = max($max_heading_depth, 5);
			}
		}

		// PRO: If max heading depth is 6 (from global settings), extract all heading levels
		// The depth filtering will happen later in the processing
		// BUT: Don't override if H5/H6 are already explicitly included (respects block-level settings)
		// This entire block is stripped from free version by Freemius
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && $max_heading_depth >= 6 && !$has_h5_or_h6) {
				$include_headings = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6');
			}
		}

		// Parse keywords into array (comma-separated)
		$keyword_list = array();
		if (!empty($exclude_keywords)) {
			$keyword_list = array_map('trim', explode(',', $exclude_keywords));
			$keyword_list = array_filter($keyword_list); // Remove empty values
		}

		// Parse "starts with" patterns into array (comma-separated)
		$pattern_list = array();
		if (!empty($exclude_patterns)) {
			$pattern_list = array_map('trim', explode(',', $exclude_patterns));
			$pattern_list = array_filter($pattern_list); // Remove empty values
		}

		// For sequential anchor format, we need a counter
		$anchor_counter = 0;
		$runtime_anchor_settings = anchorkit_get_runtime_anchor_settings();
		if ($runtime_anchor_settings && isset($runtime_anchor_settings['anchor_format'])) {
			$anchor_format = sanitize_key($runtime_anchor_settings['anchor_format']);
		} else {
			$anchor_format = anchorkit_get_option('anchorkit_toc_anchor_format', 'auto');
		}
		if (!in_array($anchor_format, array('auto', 'sequential', 'prefixed'), true)) {
			$anchor_format = 'auto';
		}

		$tag_regex = implode('|', array_map('preg_quote', $include_headings));
		// Matches <h2 ...> … </h2> with any attributes & inner HTML.
		$pattern = '/<(' . $tag_regex . ')([^>]*)>(.*?)<\/\1>/is';

		// Debug: TOC Regex Pattern and Content Length (disabled for production)

		// Check for specific Gutenberg structure if no headings are found with standard pattern
		preg_match_all($pattern, $content, $test_matches);

		// If no headings found, try Gutenberg format - add specific debug version
		if (count($test_matches[0]) === 0) {
			// Try an alternate pattern for Gutenberg
			$alt_pattern = '/<(' . $tag_regex . ')[^>]*class=["\'][^"\']*wp-block-heading[^"\']*["\'][^>]*>(.*?)<\/\1>/is';
			preg_match_all($alt_pattern, $content, $alt_matches);

			if (count($alt_matches[0]) > 0) {
				// Replace the pattern to use the Gutenberg pattern
				$pattern = $alt_pattern;
			}
		}

		$content = preg_replace_callback(
			$pattern,
			function ($m) use (&$headings, $exclude_selectors, $keyword_list, $pattern_list, $min_heading_depth, $max_heading_depth, &$anchor_counter, $anchor_format) {
				$tag = strtolower($m[1]);
				$attributes = $m[2];
				$innerHTML = $m[3];

				// Strip tags and decode HTML entities to get clean text
				$text = trim(wp_strip_all_tags($innerHTML));
				$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
				// Normalize apostrophes and quotes to standard versions
				$text = str_replace(
					array('&#039;', '&apos;', "\u{2018}", "\u{2019}", '&#8217;', "\u{201C}", "\u{201D}", '&ldquo;', '&rdquo;'),
					array("'", "'", "'", "'", "'", '"', '"', '"', '"'),
					$text
				);
				$text = trim($text);

				$original_text = $text; // Save original for anchor generation
	
				if ($text === '') {
					return $m[0];
				}

				// PRO: Check if heading is within min/max depth range
				// This entire block is stripped from free version by Freemius
				if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
					if (anchorkit_fs()->can_use_premium_code()) {
						$heading_level = intval(substr($tag, 1)); // Extract number from h1, h2, etc.
						if ($heading_level < $min_heading_depth || $heading_level > $max_heading_depth) {
							return $m[0]; // Skip this heading
						}
					}
				}

				// Simple selector exclusion (exact .class or #id match on the element itself).
				if ($exclude_selectors) {
					$selectors = array_map('trim', explode(',', $exclude_selectors));
					foreach ($selectors as $sel) {
						if ($sel === '') {
							continue;
						}
						if ($sel[0] === '.') {
							$class = preg_quote(substr($sel, 1), '/');
							if (preg_match("/class=\"[^\"]*{$class}[^\"]*\"/i", $attributes)) {
								return $m[0];
							}
						}

						if ($sel[0] === '#') {
							$id = preg_quote(substr($sel, 1), '/');
							if (preg_match("/id=\"{$id}\"/i", $attributes)) {
								return $m[0];
							}
						}
					}
				}

				// PRO: Keyword exclusion (case-insensitive partial matching)
				// This entire block is stripped from free version by Freemius
				if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
					if (anchorkit_fs()->can_use_premium_code() && !empty($keyword_list)) {
						foreach ($keyword_list as $keyword) {
							if (empty($keyword)) {
								continue;
							}
							// Case-insensitive search for keyword in heading text
							if (stripos($text, $keyword) !== false) {
								return $m[0]; // Skip this heading entirely
							}
						}
					}
				}

				// PRO: Trim patterns from heading text (for TOC display only)
				// This entire block is stripped from free version by Freemius
				if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
					if (anchorkit_fs()->can_use_premium_code() && !empty($pattern_list)) {
						foreach ($pattern_list as $pattern) {
							if (empty($pattern)) {
								continue;
							}

							// Prepare regex pattern
							$regex_pattern = preg_quote($pattern, '/');
							// Handle smart quotes (replace straight quote with character class matching straight and smart quotes)
							$regex_pattern = str_replace("'", "[''']", $regex_pattern);

							// Add word boundaries if pattern starts/ends with a word character
							if (preg_match('/^\w/u', $pattern)) {
								$regex_pattern = '\\b' . $regex_pattern;
							}
							if (preg_match('/\w$/u', $pattern)) {
								$regex_pattern = $regex_pattern . '\\b';
							}

							// Full regex: pattern + optional following numbers/punctuation/spaces
							// Example: "Step 1: Title" -> removes "Step 1: "
							$full_regex = '/' . $regex_pattern . '\s*\d*[:.\-\)\]]*\s*/iu';

							// Perform replacement
							$text = preg_replace($full_regex, '', $text);
						}

						// Clean up any remaining extra whitespace
						$text = trim($text);
					}
				}

				// Ensure an id attribute exists (use original text for anchor, not trimmed)
				// IMPORTANT: For sequential and prefixed formats, always regenerate IDs to ensure consistency
				// Only preserve existing IDs when using auto format
				$has_existing_id = preg_match('/\sid=["\\\']([^"\\\']+)["\\\']/', $attributes, $idMatch);
				$should_regenerate_id = ($anchor_format !== 'auto') || !$has_existing_id;

				if ($should_regenerate_id) {
					// Generate new anchor based on format setting
					$anchor = anchorkit_toc_generate_anchor($original_text);

					// For sequential format, add an incrementing counter
					if ($anchor_format === 'sequential') {
						$anchor_counter++;
						$anchor = $anchor . '-' . $anchor_counter;
					}

					// Remove existing ID if present and add new one
					if ($has_existing_id) {
						$attributes = preg_replace('/\sid=["\\\']([^"\\\']+)["\\\']/', '', $attributes);
					}
					$attributes .= ' id="' . esc_attr($anchor) . '"';
				} else {
					// Preserve existing ID (auto format only)
					$anchor = $idMatch[1];
				}

				$level = (int) str_replace('h', '', $tag);
				$headings[] = array(
					'level' => $level,
					'text' => $text,
					'anchor' => $anchor,
				);

				return '<' . $tag . $attributes . '>' . $innerHTML . '</' . $tag . '>';
			},
			$content
		);

		return array(
			'headings' => $headings,
			'content' => $content,
		);
	}
}


if (!function_exists('anchorkit_toc_calculate_section_word_counts')) {
	/**
	 * Calculate word counts for each section in the content
	 *
	 * @param string $content The full content
	 * @param array  $headings Array of headings with anchors
	 * @return array Updated headings array with word_count for each
	 */
	function anchorkit_toc_calculate_section_word_counts($content, $headings)
	{
		if (empty($headings) || empty($content)) {
			return $headings;
		}

		// For each heading, count words from that heading to the next heading
		for ($i = 0; $i < count($headings); $i++) {
			$current_anchor = $headings[$i]['anchor'];
			$next_anchor = isset($headings[$i + 1]) ? $headings[$i + 1]['anchor'] : null;

			// Find the position of current heading's anchor
			$start_pos = strpos($content, 'id="' . $current_anchor . '"');

			if ($start_pos === false) {
				$headings[$i]['word_count'] = 0;
				continue;
			}

			// Find the position of next heading (or end of content)
			if ($next_anchor) {
				$end_pos = strpos($content, 'id="' . $next_anchor . '"', $start_pos);
				if ($end_pos === false) {
					$section_content = substr($content, $start_pos);
				} else {
					$section_content = substr($content, $start_pos, $end_pos - $start_pos);
				}
			} else {
				// Last heading - get everything after it
				$section_content = substr($content, $start_pos);
			}

			// Strip HTML tags and count words
			$text_only = wp_strip_all_tags($section_content);
			$text_only = trim($text_only);

			// Count words
			$word_count = 0;
			if (!empty($text_only)) {
				$word_count = str_word_count($text_only);
			}

			$headings[$i]['word_count'] = $word_count;
		}

		return $headings;
	}
}


/**
 * Check if current page is AMP
 *
 * @return bool True if AMP page
 */
if (!function_exists('anchorkit_is_amp')) {
	function anchorkit_is_amp()
	{
		// Check for official AMP plugin
		if (function_exists('is_amp_endpoint')) {
			return is_amp_endpoint();
		}

		// Check for AMP for WP plugin
		if (function_exists('amp_is_request')) {
			return amp_is_request();
		}

		// Check for AMP query parameter
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- AMP detection is read-only, not state-changing
		if (isset($_GET['amp']) || isset($_GET['AMP'])) {
			return true;
		}

		// Check for /amp/ in URL
		$request_uri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '';
		if ($request_uri && preg_match('#/amp/?$#i', $request_uri)) {
			return true;
		}

		return false;
	}
}
/**
 * Generate simplified AMP-compatible TOC
 * No JavaScript, inline CSS only, simplified markup
 *
 * @param array $headings Array of headings
 * @return string AMP-compatible HTML
 */
if (!function_exists('anchorkit_generate_amp_toc')) {
	function anchorkit_generate_amp_toc($headings)
	{
		if (empty($headings)) {
			return '';
		}

		$title_text = anchorkit_get_option('anchorkit_toc_title_text', 'Table of Contents');
		$show_label = (bool) anchorkit_get_option('anchorkit_toc_show_label', true);
		$preset = anchorkit_get_option('anchorkit_toc_style_preset', 'minimal');

		// Inline CSS for AMP (no external stylesheets allowed)
		$tokens = function_exists('anchorkit_collect_toc_tokens') ? anchorkit_collect_toc_tokens() : array();
		$light_tokens = (is_array($tokens) && isset($tokens['light']) && is_array($tokens['light'])) ? $tokens['light'] : array();

		$bg_color = isset($light_tokens['--anchorkit-toc-bg']) ? (string) $light_tokens['--anchorkit-toc-bg'] : '#f8f9fa';
		$text_color = isset($light_tokens['--anchorkit-toc-text-color']) ? (string) $light_tokens['--anchorkit-toc-text-color'] : '#1a1a1a';
		$link_color = isset($light_tokens['--anchorkit-toc-link-color']) ? (string) $light_tokens['--anchorkit-toc-link-color'] : '#0073AA';
		$border_color = isset($light_tokens['--anchorkit-toc-border-color']) ? (string) $light_tokens['--anchorkit-toc-border-color'] : '#e1e4e8';

		$inline_css = "
            background-color: {$bg_color};
            color: {$text_color};
            border: 1px solid {$border_color};
            border-radius: 8px;
            padding: 20px;
            margin: 20px 0;
            max-width: 100%;
        ";

		$title_css = "
            margin: 0 0 16px 0;
            font-size: 1.25em;
            font-weight: 600;
            color: {$text_color};
        ";

		$list_css = '
            list-style: none;
            margin: 0;
            padding: 0;
        ';

		$link_css = "
            display: block;
            padding: 8px 0;
            color: {$link_color};
            text-decoration: none;
        ";

		// Build HTML
		$html = '<nav class="anchorkit-toc-amp" style="' . esc_attr($inline_css) . '" aria-label="Table of Contents">';

		if ($show_label) {
			$html .= '<div class="anchorkit-toc-title" style="' . esc_attr($title_css) . '">' . esc_html($title_text) . '</div>';
		}

		$html .= '<ul style="' . esc_attr($list_css) . '">';

		foreach ($headings as $heading) {
			$id = $heading['id'] ?? '';
			$text = $heading['text'] ?? '';
			$level = $heading['level'] ?? 'h2';

			if (empty($id) || empty($text)) {
				continue;
			}

			// Calculate padding based on heading level for indentation
			$level_num = is_numeric($level) ? (int) $level : (int) str_replace('h', '', $level);
			$padding_left = max(0, ($level_num - 2) * 16); // 0px for h2, 16px for h3, etc.

			// Build inline style for list item with proper padding
			$item_style = "margin: 0; padding: 0; padding-left: {$padding_left}px;";

			$html .= '<li style="' . esc_attr($item_style) . '">';
			$html .= '<a href="#' . esc_attr($id) . '" style="' . esc_attr($link_css) . '">' . esc_html($text) . '</a>';
			$html .= '</li>';
		}

		$html .= '</ul>';
		$html .= '</nav>';

		return $html;
	}
}
if (!function_exists('anchorkit_toc_generate_html')) {
	/**
	 * Build the <nav> TOC element.
	 *
	 * @param array  $headings Array from anchorkit_toc_parse_headings().
	 * @param string $content Optional. The full content for calculating word counts.
	 * @param array  $context Optional context for developer hooks.
	 * @return string HTML output.
	 */
	function anchorkit_toc_generate_html($headings, $content = '', $context = array())
	{
		if (function_exists('anchorkit_prepare_toc_context')) {
			$context = anchorkit_prepare_toc_context($context);
		}

		$headings = apply_filters('anchorkit_toc_headings', $headings, $context);

		if (empty($headings)) {
			return '';
		}

		$settings = array(); // Initialize settings array to avoid undefined variable error

		// Extract settings from context (for Gutenberg blocks, Elementor widgets, and shortcodes)
		if (isset($context['data']['block_settings']) && is_array($context['data']['block_settings'])) {
			$settings = $context['data']['block_settings'];
		} elseif (isset($context['data']['elementor_settings']) && is_array($context['data']['elementor_settings'])) {
			$settings = $context['data']['elementor_settings'];
		} elseif (isset($context['data']['shortcode_atts']) && is_array($context['data']['shortcode_atts'])) {
			// Map shortcode attributes to expected setting names
			$shortcode_atts = $context['data']['shortcode_atts'];
			$settings = array();

			// Map EasyTOC-style attributes to AnchorKit setting names
			if (isset($shortcode_atts['header_label'])) {
				$settings['title'] = $shortcode_atts['header_label'];
			}
			if (isset($shortcode_atts['display_header_label'])) {
				$settings['show_label'] = $shortcode_atts['display_header_label'];
			}
			if (isset($shortcode_atts['toggle_view'])) {
				$settings['collapsible'] = $shortcode_atts['toggle_view'];
			}
			if (isset($shortcode_atts['initial_view'])) {
				$settings['initial_state'] = $shortcode_atts['initial_view'] === 'hide' ? 'collapsed' : 'expanded';
			}
			if (isset($shortcode_atts['display_counter'])) {
				$settings['show_numbers'] = $shortcode_atts['display_counter'];
			}
			if (isset($shortcode_atts['hierarchical'])) {
				$settings['hierarchical'] = $shortcode_atts['hierarchical'];
			}
			if (isset($shortcode_atts['theme'])) {
				$settings['theme'] = $shortcode_atts['theme'];
			}
			if (isset($shortcode_atts['preset'])) {
				$settings['style_preset'] = $shortcode_atts['preset'];
			}
			if (isset($shortcode_atts['smooth_scroll'])) {
				$settings['smooth_scroll'] = $shortcode_atts['smooth_scroll'];
			}
			if (isset($shortcode_atts['heading_levels'])) {
				$settings['heading_levels'] = $shortcode_atts['heading_levels'];
			}
			if (isset($shortcode_atts['exclude'])) {
				$settings['exclude_headings'] = $shortcode_atts['exclude'];
			}
			if (isset($shortcode_atts['class'])) {
				$settings['custom_class'] = $shortcode_atts['class'];
			}

			// PRO features
			if (isset($shortcode_atts['view_more'])) {
				$settings['view_more'] = $shortcode_atts['view_more'];
			}
			if (isset($shortcode_atts['sticky'])) {
				$settings['sticky'] = $shortcode_atts['sticky'];
			}
			if (isset($shortcode_atts['show_reading_time'])) {
				$settings['show_reading_time'] = $shortcode_atts['show_reading_time'];
			}
			if (isset($shortcode_atts['show_word_count'])) {
				$settings['show_word_count'] = $shortcode_atts['show_word_count'];
			}
		}

		// PRO: Check if we're on an AMP page - free version defaults
		$is_amp_page = function_exists('anchorkit_is_amp') && anchorkit_is_amp();
		$amp_enabled = false;

		// Premium feature overrides - this entire block is stripped from free version
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$amp_enabled = (bool) anchorkit_get_option('anchorkit_toc_amp_enabled', false);
			}
		}

		// If on AMP page, check if AMP support is enabled
		if ($is_amp_page) {
			if (!$amp_enabled) {
				// AMP support is disabled - check if pro version
				if (anchorkit_fs() && anchorkit_fs()->is__premium_only() && anchorkit_fs()->can_use_premium_code()) {
					return '<!-- AnchorKit: Enable AMP Compatibility in Pro settings (Structure & SEO tab) to display TOC on AMP pages. -->';
				}
				// Free version message
				return '<!-- AnchorKit: AMP Compatibility requires AnchorKit Pro. Visit plugin settings to upgrade. -->';
			}
			// AMP support enabled - render AMP-compatible version
			if (function_exists('anchorkit_generate_amp_toc')) {
				$amp_headings = anchorkit_prepare_amp_headings($headings);
				return anchorkit_generate_amp_toc($amp_headings);
			}
		}

		// Load style / behaviour options (with shortcode/elementor overrides)
		$title_text = isset($settings['title']) ? $settings['title'] : anchorkit_get_option('anchorkit_toc_title_text', 'Table of Contents');

		/**
		 * Filter the TOC title text.
		 *
		 * @since 1.0.0
		 * @param string $title_text The TOC title.
		 * @param array  $context    The TOC context array.
		 */
		$title_text = apply_filters('anchorkit_title_text', $title_text, $context);

		$show_label = isset($settings['show_label']) ? (bool) $settings['show_label'] : (bool) anchorkit_get_option('anchorkit_toc_show_label', true);
		$collapsible = isset($settings['collapsible']) ? (bool) $settings['collapsible'] : (bool) anchorkit_get_option('anchorkit_toc_collapsible', true);
		$initial_state = isset($settings['initial_state']) ? $settings['initial_state'] : anchorkit_get_option('anchorkit_toc_initial_state', 'expanded');
		// Load hierarchical view setting with proper fallback (default to true only if never saved)
		$hierarchical = isset($settings['hierarchical']) ? (bool) $settings['hierarchical'] : (
			($hierarchical_option = anchorkit_get_option('anchorkit_toc_hierarchical_view', 'not_set')) === 'not_set' ? true : (bool) $hierarchical_option
		);
		$show_nums = isset($settings['show_numbers']) ? (bool) $settings['show_numbers'] : (bool) anchorkit_get_option('anchorkit_toc_show_numerals', false);
		$aria_label_custom = anchorkit_get_option('anchorkit_toc_aria_label', '');
		$hide_mobile = (bool) anchorkit_get_option('anchorkit_toc_hide_on_mobile', false);
		$toc_theme = isset($settings['theme']) ? $settings['theme'] : anchorkit_get_option('anchorkit_toc_theme', 'system');
		$theme_class = ($toc_theme === 'light' || $toc_theme === 'dark')
			? 'anchorkit-toc-theme-' . esc_attr($toc_theme)
			: 'anchorkit-toc-theme-system';

		$style_preset = isset($settings['style_preset']) ? $settings['style_preset'] : anchorkit_get_option('anchorkit_toc_style_preset', 'minimal');

		// Determine if custom styling should be used
		// For Gutenberg blocks, use custom styling when design_override is enabled
		// For other contexts (Elementor, auto-insert), use global setting
		$context_source = isset($context['source']) ? $context['source'] : '';
		if ($context_source === 'gutenberg_block' && isset($settings['design_override']) && $settings['design_override']) {
			$custom_styling = true;
		} else {
			$custom_styling = anchorkit_get_option('anchorkit_toc_custom_styling', false);
		}
		$bullet_style = anchorkit_get_option('anchorkit_toc_bullet_style', 'disc');
		$numbering_style = anchorkit_get_option('anchorkit_toc_numbering_style', 'hierarchical');
		// New: numbering format and separator
		$numbering_format = anchorkit_get_option('anchorkit_toc_numbering_format', 'decimal'); // decimal|decimal_leading_zero|upper_roman|lower_roman|upper_alpha|lower_alpha
		$numbering_sep = anchorkit_get_option('anchorkit_toc_numbering_separator', '.');
		$exclude_regex = '';

		// PRO: Get sticky settings - free version defaults
		$sticky = false;
		$sticky_position = 'content';
		$sticky_offset = 20;

		// Premium feature overrides for sticky - this block is stripped from free version
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$exclude_regex = anchorkit_get_option('anchorkit_toc_exclude_regex', '');
				$sticky = isset($settings['sticky']) ? (bool) $settings['sticky'] : (bool) anchorkit_get_option('anchorkit_toc_sticky_enabled', false);
				$sticky_position = isset($settings['sticky_position']) ? $settings['sticky_position'] : anchorkit_get_option('anchorkit_toc_sticky_position', 'content');
				$sticky_offset = isset($settings['sticky_offset']) ? (int) $settings['sticky_offset'] : (int) anchorkit_get_option('anchorkit_toc_sticky_offset', 20);
			}
		}

		// Validate sticky position
		if (!in_array($sticky_position, array('content', 'left', 'right'), true)) {
			$sticky_position = 'content';
		}

		if (!isset($context['render_settings']) || !is_array($context['render_settings'])) {
			$context['render_settings'] = array();
		}
		$context['render_settings'] = array_merge(
			$context['render_settings'],
			array(
				'title' => $title_text,
				'show_label' => $show_label,
				'collapsible' => $collapsible,
				'hierarchical' => $hierarchical,
				'show_numbers' => $show_nums,
				'theme' => $toc_theme,
				'style_preset' => $style_preset,
				'custom_styling' => $custom_styling,
				'bullet_style' => $bullet_style,
				'numbering_style' => $numbering_style,
				'numbering_format' => $numbering_format,
				'hide_on_mobile' => $hide_mobile,
			)
		);

		$mobile_breakpoint = (int) anchorkit_get_option('anchorkit_toc_mobile_breakpoint', 782);

		// Custom Labels (JSON mapping) - Replace heading text in TOC only
		$custom_labels = array();
		$custom_labels_json = '';
		if (isset($settings['custom_labels']) && $settings['custom_labels'] !== '') {
			if (is_array($settings['custom_labels'])) {
				foreach ($settings['custom_labels'] as $key => $value) {
					$custom_labels[trim((string) $key)] = trim((string) $value);
				}
			} else {
				$custom_labels_json = $settings['custom_labels'];
			}
		} else {
			$custom_labels_json = anchorkit_get_option('anchorkit_toc_custom_labels', '');
		}

		if (!empty($custom_labels_json) && empty($custom_labels)) {
			// Remove WordPress slashes before parsing JSON
			$custom_labels_json = wp_unslash($custom_labels_json);

			$decoded = json_decode($custom_labels_json, true);
			if (is_array($decoded) && json_last_error() === JSON_ERROR_NONE) {
				// Normalize keys for more flexible matching (trim whitespace)
				foreach ($decoded as $key => $value) {
					$custom_labels[trim($key)] = trim($value);
				}
			} else {
				// Debug logging for JSON errors
				if (defined('WP_DEBUG') && WP_DEBUG) {
					anchorkit_debug_log('AnchorKit JSON Error: ' . json_last_error_msg() . ' | Raw JSON: ' . $custom_labels_json);
				}
			}
		}

		// Only add preset class if custom styling is disabled
		// When custom styling is enabled, add custom class instead
		$classes = array('anchorkit-toc-container');
		if ($custom_styling) {
			$classes[] = 'anchorkit-toc-custom-styling';
		} else {
			$classes[] = 'anchorkit-toc-preset-' . esc_attr($style_preset);
		}
		// Always add theme class for both presets and custom styling, if it's set
		if (!empty($theme_class)) {
			$classes[] = $theme_class;
		}
		if ($collapsible) {
			$classes[] = 'anchorkit-toc-collapsible';
			$classes[] = ($initial_state === 'collapsed') ? 'anchorkit-toc-collapsed' : 'anchorkit-toc-expanded';
		}
		if ($hierarchical) {
			$classes[] = 'anchorkit-toc-hierarchical';
		}
		if ($show_nums) {
			$classes[] = 'anchorkit-toc-numerals';
		}
		if ($hide_mobile) {
			$classes[] = 'anchorkit-toc-hide-mobile';
		}
		if ($bullet_style) {
			$classes[] = 'anchorkit-toc-bullet-' . $bullet_style;
		}
		if ($numbering_style) {
			$classes[] = 'anchorkit-toc-numbering-' . $numbering_style;
		}

		// PRO: Add sticky classes if enabled
		// This block is stripped from free version by Freemius
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && $sticky) {
				$classes[] = 'anchorkit-toc-sticky';
				$classes[] = 'anchorkit-toc-sticky-' . $sticky_position;
			}
		}

		// PRO: Custom ID and CSS Classes - free version defaults
		$custom_id = '';

		// Premium feature overrides - this block is stripped from free version
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				// Get custom ID (separate field)
				$custom_id_raw = anchorkit_get_option('anchorkit_toc_custom_id', '');
				if (!empty($custom_id_raw)) {
					$custom_id = sanitize_html_class(trim($custom_id_raw));
				}

				// Get custom classes (from settings or global options, space-separated)
				$custom_class = isset($settings['custom_class']) ? $settings['custom_class'] : anchorkit_get_option('anchorkit_toc_custom_class', '');
				if (!empty($custom_class)) {
					$custom_items = explode(' ', $custom_class);
					foreach ($custom_items as $item) {
						$item = trim($item);
						if (empty($item)) {
							continue;
						}

						// Remove . prefix if present
						$class_value = strpos($item, '.') === 0 ? substr($item, 1) : $item;
						$sanitized = sanitize_html_class($class_value);
						if (!empty($sanitized)) {
							$classes[] = $sanitized;
						}
					}
				}
			}
		}

		$classes = apply_filters('anchorkit_toc_container_classes', $classes, $context);
		// Backward compatibility with older filter name.
		$classes = apply_filters('anchorkit_toc_container_class', $classes);

		// PRO: Reading Time & Metadata - free version defaults
		$show_reading_time = false;
		$show_word_count = false;
		$reading_speed = 200;
		$time_format = 'min_read';

		// Calculate word counts if needed and content is provided
		// This entire block is stripped from free version by Freemius
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$show_reading_time = isset($settings['show_reading_time']) ? (bool) $settings['show_reading_time'] : (bool) anchorkit_get_option('anchorkit_toc_show_reading_time', false);
				$show_word_count = isset($settings['show_word_count']) ? (bool) $settings['show_word_count'] : (bool) anchorkit_get_option('anchorkit_toc_show_word_count', false);
				$reading_speed = (int) anchorkit_get_option('anchorkit_toc_reading_speed', 200);
				$time_format = anchorkit_get_option('anchorkit_toc_time_format', 'min_read');

				if (($show_reading_time || $show_word_count) && !empty($content)) {
					$headings = anchorkit_toc_calculate_section_word_counts($content, $headings);
				}
			}
		}

		// Debug: Add HTML comment showing custom labels (visible in page source)
		$debug_comment = '';
		if (!empty($custom_labels)) {
			$debug_comment = '<!-- AnchorKit Custom Labels: ' . esc_html(json_encode($custom_labels)) . ' -->';
		}

		// Build inline styles for bullet customization
		$inline_styles = array();
		// Note: Bullet colors are handled via CSS variables in style blocks, not inline styles
		// Inline styles would override media queries for dark mode
		if ($bullet_style === 'custom_character') {
			$bullet_character = anchorkit_get_option('anchorkit_toc_bullet_character', '•');
			$inline_styles[] = '--anchorkit-toc-custom-bullet: "' . esc_attr($bullet_character) . '"';
		}

		// Safari fix: Add data attribute to trigger client-side inline styling
		// This is more reliable than CSS for Safari's buggy CSS variable handling
		$data_attrs = array();
		if ($toc_theme === 'system') {
			$data_attrs[] = 'data-anchorkit-auto-theme="1"';
		}

		if ($hide_mobile) {
			$data_attrs[] = 'data-mobile-breakpoint="' . (int) $mobile_breakpoint . '"';
		}

		// PRO: Add sticky data attributes for JavaScript
		// This block is stripped from free version by Freemius
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && $sticky) {
				$data_attrs[] = 'data-sticky="' . ($sticky ? '1' : '0') . '"';
				$data_attrs[] = 'data-sticky-position="' . esc_attr($sticky_position) . '"';
				$data_attrs[] = 'data-sticky-offset="' . esc_attr($sticky_offset) . '"';
			}
		}

		ob_start();
		do_action('anchorkit_toc_before', $context);
		$before_hook_output = ob_get_clean();

		$nav = $debug_comment;
		if (!empty($before_hook_output)) {
			$nav = $before_hook_output . $nav;
		}
		$nav .= '<nav class="' . esc_attr(implode(' ', $classes)) . '"';
		if (!empty($custom_id)) {
			$nav .= ' id="' . esc_attr($custom_id) . '"';
		}
		if (!empty($inline_styles)) {
			$nav .= ' style="' . esc_attr(implode('; ', $inline_styles)) . '"';
		}
		if (!empty($data_attrs)) {
			$nav .= ' ' . implode(' ', $data_attrs);
		}
		$nav .= ' aria-label="' . esc_attr($aria_label_custom ?: ($title_text ?: __('Table of Contents', 'anchorkit-table-of-contents'))) . '"';
		$nav .= ' role="navigation"';
		$nav .= '>';

		if ($show_label && $title_text !== '') {
			$nav .= '<div class="anchorkit-toc-title" tabindex="0" role="button">' . esc_html($title_text);
			if ($collapsible) {
				$expanded = $initial_state === 'collapsed' ? 'false' : 'true';
				$nav .= '<button class="anchorkit-toc-toggle-button" aria-expanded="' . esc_attr($expanded) . '" aria-controls="anchorkit-toc-list"><span class="anchorkit-toc-toggle-icon"></span><span class="screen-reader-text">' . esc_html__('Toggle Table of Contents', 'anchorkit-table-of-contents') . '</span></button>';
			}
			$nav .= '</div>';
		}

		// Add unique ID for the TOC list (for aria-controls)
		$toc_list_id = 'anchorkit-toc-list-' . uniqid();

		// Build list (using flat structure like Elementor for consistency)
		$nav .= '<ul id="' . esc_attr($toc_list_id) . '" class="anchorkit-toc-list">';
		$normalize_level = static function ($value) {
			if (is_numeric($value)) {
				$level = (int) $value;
			} else {
				$level = (int) preg_replace('/[^0-9]/', '', strtolower((string) $value));
			}
			if ($level < 1 || $level > 6) {
				$level = 2;
			}
			return $level;
		};
		$base_level = $hierarchical ? $normalize_level($headings[0]['level'] ?? 2) : 2;
		$num_tracker = array(0, 0, 0, 0, 0, 0); // Track the numbering for each level
		$auto_trim_patterns = anchorkit_toc_get_auto_trim_patterns($show_nums, $numbering_format, $numbering_sep);

		// PRO: Get View More settings early so we can hide items in the loop
		// Free version defaults
		$view_more_enabled = false;
		$initial_count = 0;

		// Premium feature overrides - this entire block is stripped from free version
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$view_more_enabled = isset($settings['view_more']) ?
					true : // Shortcode enabled view_more
					(bool) anchorkit_get_option('anchorkit_toc_view_more_enabled', false);
				$initial_count = isset($settings['view_more']) ?
					intval($settings['view_more']) : // Use shortcode value
					($view_more_enabled ? intval(anchorkit_get_option('anchorkit_toc_initial_count', 5)) : 0);
			}
		}

		foreach ($headings as $index => $h) {
			$level = $normalize_level($h['level'] ?? 2);
			// Calculate depth for CSS classes, aria-level, and numbering logic
			// Depth is relative to base level (1 = top level, 2 = first sub-level, etc.)
			$depth = $hierarchical ? max(1, $level - $base_level + 1) : 1;
			$anchor = $h['anchor'] ?? ($h['id'] ?? '');
			if ($anchor === '') {
				continue;
			}

			// Numerals
			$prefix = '';
			if ($show_nums && $level >= 1 && $level <= 6) {
				$prefix = anchorkit_toc_build_number_prefix(
					$num_tracker,
					$level,
					$base_level,
					$hierarchical,
					$numbering_style,
					$numbering_format,
					$numbering_sep
				);
			}

			// Calculate the position for keyboard navigation
			$position = $index + 1;
			$total = count($headings);

			// Build the list item classes
			$item_classes = array(
				'anchorkit-toc-item',
				'anchorkit-toc-level-' . $depth,
				'anchorkit-toc-heading-level-' . $level,
			);

			// Add hidden class for items beyond initial count when View More is enabled
			if ($view_more_enabled && $index >= $initial_count) {
				$item_classes[] = 'anchorkit-toc-hidden-item';
			}

			// Build the list item with proper accessibility attributes (flat structure)
			// Use depth for both CSS class and aria-level (1 = top level, 2 = first sub-level, etc.)
			$nav .= '<li class="' . esc_attr(implode(' ', $item_classes)) . '" data-toc-index="' . esc_attr($index) . '">';

			$link_attributes = array(
				'class' => 'anchorkit-toc-link',
				'href' => '#' . ltrim((string) $anchor, '#'),
				'aria-setsize' => (string) $total,
				'aria-posinset' => (string) $position,
				'aria-level' => (string) $depth,
			);

			$link_attributes = apply_filters('anchorkit_toc_link_attributes', $link_attributes, $h, $context);

			$nav .= '<a';
			foreach ($link_attributes as $attr => $value) {
				if ($value === null || $value === false || $attr === '') {
					continue;
				}
				if ($value === true) {
					$nav .= ' ' . esc_attr($attr);
					continue;
				}
				$nav .= ' ' . esc_attr($attr) . '="' . esc_attr($value) . '"';
			}
			$nav .= '>';

			if ($prefix) {
				$nav .= '<span class="anchorkit-toc-item-number" aria-hidden="true">' . esc_html($prefix) . '</span>';
				// For screen readers, announce the level and number together with the text
				// translators: %1$d is the heading level (1-6), %2$s is the item number (e.g., "1.2.3")
				$sr_prefix = sprintf(__('Level %1$d, item %2$s', 'anchorkit-table-of-contents'), $depth, str_replace('.', ' point ', $prefix));
				$nav .= '<span class="screen-reader-text">' . esc_html($sr_prefix) . '</span>';
			}

			// Apply custom label if it exists (replaces TOC text only, not the actual heading on the page)
			$display_text = $h['text'];
			$display_text = anchorkit_toc_trim_heading_prefixes($display_text, $exclude_regex, $auto_trim_patterns);
			$trimmed_heading = trim($h['text']);

			// Normalize both the heading and the keys for better matching
			// This handles different apostrophe types, quotes, and HTML entities
			$normalized_heading = html_entity_decode($trimmed_heading, ENT_QUOTES | ENT_HTML5, 'UTF-8');
			$normalized_heading = str_replace(
				array('&#039;', '&apos;', "\u{2018}", "\u{2019}", '&#8217;', "\u{201C}", "\u{201D}", '&ldquo;', '&rdquo;'),
				array("'", "'", "'", "'", "'", '"', '"', '"', '"'),
				$normalized_heading
			);
			$normalized_heading = trim($normalized_heading);

			// Check if there's a match
			$matched = false;
			foreach ($custom_labels as $key => $value) {
				// Normalize the key the same way
				$normalized_key = html_entity_decode($key, ENT_QUOTES | ENT_HTML5, 'UTF-8');
				$normalized_key = str_replace(
					array('&#039;', '&apos;', "\u{2018}", "\u{2019}", '&#8217;', "\u{201C}", "\u{201D}", '&ldquo;', '&rdquo;'),
					array("'", "'", "'", "'", "'", '"', '"', '"', '"'),
					$normalized_key
				);
				$normalized_key = trim($normalized_key);

				if (strtolower($normalized_heading) === strtolower($normalized_key)) {
					$display_text = $value;
					$matched = true;
					break;
				}
			}

			/**
			 * Filter the heading text displayed in the TOC.
			 *
			 * @since 1.0.0
			 * @param string $display_text The text to display in the TOC.
			 * @param array  $heading      The heading data array (text, level, anchor, etc.).
			 * @param array  $context      The TOC context array.
			 */
			$display_text = apply_filters('anchorkit_heading_text', $display_text, $h, $context);

			$nav .= '<span class="anchorkit-toc-item-text">' . esc_html($display_text) . '</span>';

			// PRO: Add reading time and/or word count metadata
			// This block is stripped by Freemius in the free version
			if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
				if (anchorkit_fs()->can_use_premium_code() && isset($h['word_count']) && $h['word_count'] > 0) {
					$metadata_parts = array();

					// Calculate and format reading time
					if ($show_reading_time) {
						$minutes = max(1, round($h['word_count'] / $reading_speed));

						switch ($time_format) {
							case 'minutes':
								$time_str = sprintf('~%d minute%s', $minutes, $minutes > 1 ? 's' : '');
								break;
							case 'short':
								$time_str = $minutes . 'm';
								break;
							case 'min_read':
							default:
								$time_str = sprintf('%d min read', $minutes);
								break;
						}
						$metadata_parts[] = $time_str;
					}

					// Add word count
					if ($show_word_count) {
						$metadata_parts[] = number_format($h['word_count']) . ' words';
					}

					// Output metadata if we have any
					if (!empty($metadata_parts)) {
						$nav .= '<span class="anchorkit-toc-metadata">(' . esc_html(implode(' • ', $metadata_parts)) . ')</span>';
					}
				}
			}

			$nav .= '</a>';
			$nav .= '</li>';
		}

		$nav .= '</ul>';

		// PRO: Add View More button if enabled (moved outside scrollable list)
		// This block is stripped by Freemius in the free version
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$shortcode_view_more = isset($settings['view_more']) ? intval($settings['view_more']) : false;
				if ($shortcode_view_more || anchorkit_get_option('anchorkit_toc_view_more_enabled', false)) {
					$initial_count = $shortcode_view_more ?: intval(anchorkit_get_option('anchorkit_toc_initial_count', 5));
					$view_more_text = anchorkit_get_option('anchorkit_toc_view_more_text', 'View More');
					$view_less_text = anchorkit_get_option('anchorkit_toc_view_less_text', 'View Less');

					// Only add button if we have more items than the initial count
					if (count($headings) > $initial_count) {
						$nav .= '<div class="anchorkit-toc-view-more-item">';
						$nav .= '<button class="anchorkit-toc-view-more-btn" ';
						$nav .= 'data-initial-count="' . esc_attr($initial_count) . '" ';
						$nav .= 'data-view-more-text="' . esc_attr($view_more_text) . '" ';
						$nav .= 'data-view-less-text="' . esc_attr($view_less_text) . '" ';
						$nav .= 'aria-expanded="false" ';
						$nav .= 'type="button">';
						$nav .= '<span class="anchorkit-toc-view-more-icon"></span>';
						$nav .= '<span class="anchorkit-toc-view-more-text">' . esc_html($view_more_text) . '</span>';
						$nav .= '</button>';
						$nav .= '</div>';
					}
				}
			}
		}

		// Back-to-top link (PRO FEATURE) - Only shown when TOC is sticky
		// This block is stripped by Freemius in the free version
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code()) {
				$sticky_enabled = (bool) anchorkit_get_option('anchorkit_toc_sticky_enabled', false);
				if ($sticky_enabled && anchorkit_get_option('anchorkit_toc_back_to_top_link', false)) {
					$back_to_top_text = anchorkit_get_option('anchorkit_toc_back_to_top_text', 'Back to Top');
					$back_to_top_font_size = anchorkit_get_option('anchorkit_toc_back_to_top_font_size', 14);
					// Prepend arrow if not already present in the text
					$back_to_top_display_text = (strpos($back_to_top_text, '↑') === false) ? '↑ ' . $back_to_top_text : $back_to_top_text;
					$nav .= '<div class="anchorkit-toc-back-to-top" style="--anchorkit-toc-back-to-top-font-size: ' . esc_attr($back_to_top_font_size) . 'px;"><a href="#" aria-label="' . esc_attr__('Scroll to the top of the page', 'anchorkit-table-of-contents') . '">' . esc_html($back_to_top_display_text) . '</a></div>';
				}
			}
		}

		$nav .= '</nav>';

		// Safari fix: Apply inline styles directly via JavaScript (100% reliable)
		// This bypasses CSS cascade issues in Safari
		/**
		 * COLOR SYSTEM ARCHITECTURE - Auto-Insert TOC JavaScript Dark Mode Fix
		 * =====================================================================
		 *
		 * This JavaScript applies dark mode colors ONLY to auto-inserted TOCs.
		 * It must NOT affect instance-specific TOCs (Elementor/Gutenberg).
		 *
		 * AUTO-INSERT TOCs (Managed by this script):
		 *   - No data-anchorkit-instance attribute
		 *   - Use global CSS variables + this JavaScript for dark mode
		 *   - Get colors from anchorkit_collect_toc_tokens()
		 *
		 * INSTANCE TOCs (Elementor/Gutenberg - SKIP):
		 *   - Have data-anchorkit-instance="<unique-id>" attribute
		 *   - Manage their own colors via scoped style blocks
		 *   - JavaScript checks hasAttribute("data-anchorkit-instance") and skips
		 *
		 * EXCLUSION LOGIC:
		 *   1. Skip if inside .anchorkit-preview-content (admin preview)
		 *   2. Skip if has .anchorkit-toc-custom-styling (user custom colors)
		 *   3. Skip if has data-anchorkit-instance (Elementor/Gutenberg widgets)
		 *
		 * See also: includes/features/toc/integrations/elementor.php lines 1387-1460
		 */

		// IMPORTANT: Skip this when custom styling is enabled, so user's custom CSS can work
		$custom_styling_enabled = filter_var(anchorkit_get_option('anchorkit_toc_custom_styling', false), FILTER_VALIDATE_BOOLEAN);

		if ($toc_theme === 'system' && !$custom_styling_enabled) {
			// Get dark mode color values
			$all_tokens = function_exists('anchorkit_collect_toc_tokens') ? anchorkit_collect_toc_tokens() : array();
			$dark_tokens = isset($all_tokens['dark']) ? $all_tokens['dark'] : array();

			if (!empty($dark_tokens)) {
				$dark_bg = isset($dark_tokens['--anchorkit-toc-bg']) ? $dark_tokens['--anchorkit-toc-bg'] : '#1e1e1e';
				$dark_text = isset($dark_tokens['--anchorkit-toc-text-color']) ? $dark_tokens['--anchorkit-toc-text-color'] : '#e5e5e5';
				$dark_border = isset($dark_tokens['--anchorkit-toc-border-color']) ? $dark_tokens['--anchorkit-toc-border-color'] : '#444444';

				// For Modern preset, use gradient
				if ($style_preset === 'modern') {
					$dark_bg = 'linear-gradient(135deg, #0d1117 0%, #161b22 50%, #1c2128 100%)';
					$dark_border = 'rgba(88, 166, 255, 0.2)';
				}

				// For Safari
				static $safari_fix_added = false;
				if (!$safari_fix_added) {
					$safari_js = '(function(){';
					$safari_js .= 'if(!window.matchMedia)return;';
					$safari_js .= 'var isDark=window.matchMedia("(prefers-color-scheme: dark)").matches;';
					$safari_js .= 'if(isDark){';
					$safari_js .= 'var tocs=document.querySelectorAll(".anchorkit-toc-container[data-anchorkit-auto-theme]");';
					$safari_js .= 'tocs.forEach(function(el){';
					$safari_js .= 'if(el.closest(".anchorkit-preview-content")) return;';
					$safari_js .= 'if(el.classList.contains("anchorkit-toc-custom-styling")) return;';
					$safari_js .= 'if(el.hasAttribute("data-anchorkit-instance")) return;';
					$safari_js .= 'var bg="' . esc_js($dark_bg) . '";';
					$safari_js .= 'if(bg.includes("linear-gradient")) el.style.background="-webkit-" + bg;';
					$safari_js .= 'el.style.background=bg;';
					$safari_js .= 'el.style.color="' . esc_js($dark_text) . '";';
					$safari_js .= 'el.style.borderColor="' . esc_js($dark_border) . '";';
					$safari_js .= '});';
					$safari_js .= '}';
					$safari_js .= '})();';

					wp_add_inline_script('anchorkit-toc-js', $safari_js);
					$safari_fix_added = true;
				}
			}
		}

		ob_start();
		do_action('anchorkit_toc_after', $context);
		$nav .= ob_get_clean();

		return apply_filters('anchorkit_toc_html', $nav, $context);
	}
}

if (!function_exists('anchorkit_toc_normalize_numbering_separator')) {
	function anchorkit_toc_normalize_numbering_separator($separator)
	{
		return in_array($separator, array('.', '-', '·'), true) ? $separator : '.';
	}
}

if (!function_exists('anchorkit_toc_format_number_segment')) {
	function anchorkit_toc_format_number_segment($n, $numbering_format, $segment_index = null)
	{
		$n = (int) $n;
		switch ($numbering_format) {
			case 'decimal_leading_zero':
				if ($segment_index === 0) {
					return str_pad((string) $n, 2, '0', STR_PAD_LEFT);
				}
				return (string) $n;
			case 'upper_roman':
				return strtoupper(anchorkit_int_to_roman($n));
			case 'lower_roman':
				return strtolower(anchorkit_int_to_roman($n));
			case 'upper_alpha':
				return anchorkit_int_to_alpha($n, true);
			case 'lower_alpha':
				return anchorkit_int_to_alpha($n, false);
			case 'decimal':
			default:
				return (string) $n;
		}
	}
}

if (!function_exists('anchorkit_toc_build_number_prefix')) {
	function anchorkit_toc_build_number_prefix(
		array &$num_tracker,
		$level,
		$base_level,
		$hierarchical,
		$numbering_style,
		$numbering_format,
		$numbering_sep
	) {
		$level = (int) $level;
		if ($level < 1 || $level > 6) {
			return '';
		}
		if ($base_level === null) {
			$base_level = $level;
		}

		$sep = anchorkit_toc_normalize_numbering_separator($numbering_sep);

		// Flat numbering: use a single global counter (stored in index 6) for all items
		if ($numbering_style === 'flat') {
			// Use index 6 as the global flat counter (indices 0-5 are for levels 1-6)
			if (!isset($num_tracker[6])) {
				$num_tracker[6] = 0;
			}
			++$num_tracker[6];
			return anchorkit_toc_format_number_segment($num_tracker[6], $numbering_format, 0) . $sep . ' ';
		}

		// Hierarchical numbering: use per-level counters
		$num_tracker[$level - 1] = ($num_tracker[$level - 1] ?? 0) + 1;
		for ($i = $level; $i < 6; $i++) {
			$num_tracker[$i] = 0;
		}

		if ($hierarchical && $numbering_style === 'hierarchical') {
			$offset = max(0, (int) $base_level - 1);
			$length = max(1, ($level - (int) $base_level) + 1);
			$relevant_levels = array_slice($num_tracker, $offset, $length);
			$last_index = count($relevant_levels) - 1;
			foreach ($relevant_levels as $idx => $value) {
				// Avoid "00" segments when heading levels are skipped.
				if ($idx < $last_index && $value === 0) {
					$relevant_levels[$idx] = 1;
				}
			}
			$formatted = array();
			foreach ($relevant_levels as $segment_index => $value) {
				$formatted[] = anchorkit_toc_format_number_segment($value, $numbering_format, $segment_index);
			}
			return implode($sep, $formatted) . $sep . ' ';
		}

		return anchorkit_toc_format_number_segment($num_tracker[$level - 1], $numbering_format, 0) . $sep . ' ';
	}
}

if (!function_exists('anchorkit_toc_get_auto_trim_patterns')) {
	function anchorkit_toc_get_auto_trim_patterns($show_numerals, $numbering_format, $numbering_sep)
	{
		if (!$show_numerals) {
			return array();
		}

		$sep = anchorkit_toc_normalize_numbering_separator($numbering_sep);
		$sep_pattern = preg_quote($sep, '/');
		$token = '\d+';
		$flags = 'u';
		switch ($numbering_format) {
			case 'upper_roman':
			case 'lower_roman':
				$token = '[ivxlcdm]+';
				$flags = 'iu';
				break;
			case 'upper_alpha':
			case 'lower_alpha':
				$token = '[a-z]+';
				$flags = 'iu';
				break;
			case 'decimal_leading_zero':
			case 'decimal':
			default:
				$token = '\d+';
				$flags = 'u';
				break;
		}

		return array(
			'/^\s*' . $token . $sep_pattern . $token . '(?:' . $sep_pattern . $token . ')*\s*[:.\-\)\]]*\s+/' . $flags,
			'/^\s*' . $token . '\s*[:.\-\)\]]+\s+/' . $flags,
		);
	}
}

if (!function_exists('anchorkit_toc_trim_heading_prefixes')) {
	function anchorkit_toc_trim_heading_prefixes($display_text, $exclude_regex, $auto_trim_patterns)
	{
		$display_text = (string) $display_text;

		// PRO: Custom regex trimming - this block is stripped by Freemius in free version
		if (anchorkit_fs() && anchorkit_fs()->is__premium_only()) {
			if (anchorkit_fs()->can_use_premium_code() && !empty($exclude_regex)) {
				$pattern_list = array_map('trim', explode(',', $exclude_regex));
				$pattern_list = array_filter($pattern_list); // Remove empty values
				foreach ($pattern_list as $pattern) {
					if ($pattern === '') {
						continue;
					}

					// Prepare regex pattern
					$regex_pattern = preg_quote($pattern, '/');
					// Handle smart quotes (replace straight quote with character class matching straight and smart quotes)
					$regex_pattern = str_replace("'", "['']", $regex_pattern);

					// Add word boundaries if pattern starts/ends with a word character
					if (preg_match('/^\w/u', $pattern)) {
						$regex_pattern = '\\b' . $regex_pattern;
					}
					if (preg_match('/\w$/u', $pattern)) {
						$regex_pattern = $regex_pattern . '\\b';
					}

					// Full regex: pattern + optional following numbers/punctuation/spaces
					// Example: "Step 1: Title" -> removes "Step 1: "
					$full_regex = '/' . $regex_pattern . '\s*\d*[:.​\-\)\]]*\s*/iu';

					// Perform replacement
					$display_text = preg_replace($full_regex, '', $display_text);
				}

				// Clean up any remaining extra whitespace
				$display_text = trim($display_text);
			}
		}

		if (!empty($auto_trim_patterns)) {
			$display_text = preg_replace($auto_trim_patterns, '', $display_text, 1);
			$display_text = trim($display_text);
		}

		return $display_text;
	}
}


/**
 * Convert integer to Roman numerals (supports 1..3999)
 */
function anchorkit_int_to_roman($num)
{
	$num = (int) $num;
	if ($num <= 0) {
		return '0';
	}
	$map = array(
		'M' => 1000,
		'CM' => 900,
		'D' => 500,
		'CD' => 400,
		'C' => 100,
		'XC' => 90,
		'L' => 50,
		'XL' => 40,
		'X' => 10,
		'IX' => 9,
		'V' => 5,
		'IV' => 4,
		'I' => 1,
	);
	$res = '';
	foreach ($map as $roman => $val) {
		while ($num >= $val) {
			$res .= $roman;
			$num -= $val;
		}
	}
	return $res;
}

/**
 * Convert integer to alphabetic sequence (1->A/a, 27->AA/aa)
 */
function anchorkit_int_to_alpha($num, $upper = true)
{
	$num = (int) $num;
	if ($num <= 0) {
		return $upper ? 'A' : 'a';
	}
	$letters = '';
	while ($num > 0) {
		--$num; // 1-based to 0-based
		$letters = chr(($num % 26) + ($upper ? 65 : 97)) . $letters;
		$num = (int) floor($num / 26);
	}
	return $letters;
}
