__( 'Contents', 'tocnest' ), 'show_title' => true, 'title_tag' => 'p', 'toggle' => true, 'label_show' => __( 'Show', 'tocnest' ), 'label_hide' => __( 'Hide', 'tocnest' ), 'collapsed' => false, 'min' => 3, 'depth' => 6, 'levels' => array( 2, 3, 4 ), 'hierarchical' => true, 'numbering' => 'decimalnested', 'suffix' => 'none', 'skip_levels' => '', 'skip_text' => '', 'skip_class' => '', 'anchor_format' => 'text', 'lowercase' => true, 'dashes' => true, 'smooth' => true, 'offset' => 24, 'highlight' => false, 'scheme' => 'light', 'background' => '', 'border' => '', 'title_color' => '', 'link' => '', 'hover' => '', 'visited' => '', 'width' => 'auto', 'float' => 'none', 'title_size' => '1.1em', 'title_weight' => '700', 'items_size' => '1em', 'markup' => 'ol', 'class' => '', 'nofollow' => false, 'auto' => true, 'post_types' => array( 'post' ), 'anchor_types' => array(), 'metabox_types' => array( 'post', 'page' ), 'position' => 'beforefirstheading', 'compat' => false, 'override_previous' => false, ); } /** * Enumerated values accepted by settings. * * @return array */ public static function enums() { return array( 'title_tag' => array( 'p', 'div' ), 'numbering' => array( 'none', 'decimal', 'decimalnested', 'roman', 'romannested' ), 'suffix' => array( 'none', 'dot', 'roundbracket' ), 'anchor_format' => array( 'text', 'text_raw', 'counter' ), 'scheme' => array( 'light', 'dark', 'white', 'transparent', 'inherit' ), 'float' => array( 'none', 'left', 'right', 'center' ), 'title_weight' => array( '100', '200', '300', '400', '500', '600', '700', '800', '900' ), 'markup' => array( 'ol', 'ul' ), 'position' => array( 'beforefirstheading', 'afterfirstheading', 'afterfirstblock', 'top', 'bottom' ), ); } /** * Convert HTML and shortcode booleans consistently. * * @param mixed $value Raw value. * @return bool */ public static function boolean( $value ) { return in_array( $value, array( true, 1, '1', 'true', 'on', 'yes' ), true ); } /** * Validate a safe CSS length without functions or declarations. * * @param string $value CSS length. * @param string $fallback Default length. * @return string */ public static function length( $value, $fallback = 'auto' ) { return is_scalar( $value ) && preg_match( '/^(?:0|(?:\d*\.)?\d+(?:px|em|rem|%|vw|vh|ch))$/i', (string) $value ) ? (string) $value : $fallback; } /** * Validate settings. Partial mode keeps only explicitly provided keys. * * @param mixed $input Raw values. * @param bool $partial Whether to omit missing keys. * @return array */ public static function sanitize( $input, $partial = false ) { $defaults = self::defaults(); $enums = self::enums(); $output = $partial ? array() : $defaults; foreach ( (array) $input as $key => $value ) { if ( ! array_key_exists( $key, $defaults ) || null === $value ) { continue; } $default = $defaults[ $key ]; if ( is_bool( $default ) ) { $output[ $key ] = self::boolean( $value ); } elseif ( isset( $enums[ $key ] ) ) { $output[ $key ] = in_array( $value, $enums[ $key ], true ) ? $value : $default; } elseif ( is_int( $default ) ) { $limits = array( 'min' => array( 1, 999 ), 'depth' => array( 1, 6 ), 'offset' => array( -1000, 2000 ), ); $output[ $key ] = is_numeric( $value ) ? max( $limits[ $key ][0], min( $limits[ $key ][1], (int) $value ) ) : $default; } elseif ( is_array( $default ) ) { $values = is_array( $value ) ? $value : ( is_scalar( $value ) ? explode( ',', (string) $value ) : $default ); $values = array_filter( $values, 'is_scalar' ); $output[ $key ] = 'levels' === $key ? array_values( array_intersect( array( 1, 2, 3, 4, 5, 6 ), array_map( 'intval', $values ) ) ) : array_values( array_unique( array_filter( array_map( 'sanitize_key', $values ) ) ) ); } elseif ( ! is_scalar( $value ) ) { $output[ $key ] = $default; } elseif ( in_array( $key, array( 'background', 'border', 'title_color', 'link', 'hover', 'visited' ), true ) ) { $output[ $key ] = sanitize_hex_color( $value ) ? sanitize_hex_color( $value ) : ''; } elseif ( in_array( $key, array( 'width', 'title_size', 'items_size' ), true ) ) { $output[ $key ] = 'width' === $key && in_array( $value, array( 'auto', 'full' ), true ) ? $value : self::length( $value, $default ); } elseif ( in_array( $key, array( 'class', 'skip_class' ), true ) ) { $output[ $key ] = implode( ' ', array_filter( array_map( 'sanitize_html_class', preg_split( '/\s+/', $value ) ) ) ); } else { $output[ $key ] = 'skip_text' === $key ? sanitize_textarea_field( $value ) : sanitize_text_field( $value ); } } foreach ( array( 'label_show', 'label_hide' ) as $key ) { if ( isset( $output[ $key ] ) && '' === $output[ $key ] ) { $output[ $key ] = $defaults[ $key ]; } } return $output; } /** * Read filtered global settings. * * @return array */ public static function get() { return self::sanitize( apply_filters( 'tocnest_settings', wp_parse_args( get_option( 'tocnest_settings', array() ), self::defaults() ) ) ); } /** * Get global settings with post overrides, including lazy migration. * * @param int $post_id Post ID. * @return array */ public static function for_post( $post_id ) { return array_merge( self::get(), self::post_overrides( $post_id ) ); } /** * Read sparse overrides, including legacy values before conversion. * * @param int $post_id Post ID. * @return array */ public static function post_overrides( $post_id ) { $meta = get_post_meta( $post_id, '_tocnest_settings', true ); if ( ! metadata_exists( 'post', $post_id, '_tocnest_settings' ) && metadata_exists( 'post', $post_id, '_lwptoc_settings' ) ) { $meta = Tocnest_Migration::map( get_post_meta( $post_id, '_lwptoc_settings', true ), true )['settings']; } return self::sanitize_meta( $meta ); } /** * Validate post overrides, preserving inheritance through absent keys. * * @param mixed $input Raw meta. * @return array */ public static function sanitize_meta( $input ) { $input = is_array( $input ) ? $input : array(); $output = self::sanitize( $input, true ); foreach ( array( 'enabled', 'process_headings' ) as $key ) { if ( isset( $input[ $key ] ) && '' !== $input[ $key ] ) { $output[ $key ] = self::boolean( $input[ $key ] ); } } return $output; } }