options = $options; $this->init_hooks(); } /** * Initialize hooks. */ private function init_hooks() { AZGL_KB::boot( AZGL_PLUGIN_URL, AZGL_VERSION ); add_filter( 'the_content', array( $this, 'process_content' ), 15 ); add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'wp_head', array( $this, 'output_schema_markup' ) ); } /** * Output Schema.org DefinedTerm markup for glossary entries. * * @since 2.3.0 */ public function output_schema_markup() { if ( ! is_singular( 'page' ) ) { return; } // Check if schema is enabled. if ( ! $this->options->get( 'enable_schema', true ) ) { return; } global $post; if ( $post && AZGL_KB::root_of( $post->ID ) ) { AZGL_KB::print_schema( $post->ID ); return; } if ( ! $post || ! $post->post_parent ) { return; } // Check if this is a glossary entry (child of glossary page). $parent_glossary = $this->options->get_glossary( $post->post_parent ); if ( ! $parent_glossary || empty( $parent_glossary['active'] ) ) { return; } $parent_page = get_post( $post->post_parent ); $description = get_the_excerpt( $post ); if ( empty( $description ) ) { $description = wp_trim_words( wp_strip_all_tags( $post->post_content ), 30, '...' ); } $schema = array( '@context' => 'https://schema.org', '@type' => 'DefinedTerm', 'name' => get_the_title( $post ), 'description' => $description, 'url' => get_permalink( $post ), 'inDefinedTermSet' => array( '@type' => 'DefinedTermSet', 'name' => $parent_page ? $parent_page->post_title : '', 'url' => $parent_page ? get_permalink( $parent_page ) : '', ), ); echo '' . "\n"; } /** * Enqueue frontend assets. */ public function enqueue_assets() { if ( ! is_singular( 'page' ) ) { return; } global $post; if ( ! $post ) { return; } // Check if this is a glossary page or child of one. $is_glossary = $this->options->is_glossary_page( $post->ID ); $is_entry = $post->post_parent && $this->options->is_glossary_page( $post->post_parent ); $is_kb = (bool) AZGL_KB::root_of( $post->ID ); if ( ! $is_glossary && ! $is_entry && ! $is_kb ) { return; } wp_enqueue_style( 'azgl-frontend', AZGL_PLUGIN_URL . 'assets/css/frontend.css', array(), AZGL_VERSION ); // Add color scheme and dark mode classes. $color_scheme = $this->options->get( 'color_scheme', 'emerald' ); $dark_mode = $this->options->get( 'dark_mode', 'auto' ); // Inline CSS for color scheme variables. $inline_css = $this->get_color_scheme_css( $color_scheme, $dark_mode ); wp_add_inline_style( 'azgl-frontend', $inline_css ); // Knowledge base mode (v2.4.0). if ( $is_kb ) { AZGL_KB::enqueue_assets( 'azgl-frontend' ); return; } // Load frontend JS for Print functionality (v2.3.0). if ( $is_glossary ) { wp_enqueue_script( 'azgl-frontend', AZGL_PLUGIN_URL . 'assets/js/frontend.js', array(), AZGL_VERSION, true ); } } /** * Process content. * * @param string $content Post content. * @return string Processed content. */ public function process_content( $content ) { // Only on frontend single pages. if ( is_admin() || ! is_singular( 'page' ) ) { return $content; } global $post; if ( ! $post ) { return $content; } // Knowledge base mode (shared with Pro). if ( AZGL_KB::root_of( $post->ID ) ) { return AZGL_KB::render( $post->ID, $content ); } // Check if this is a glossary page. $glossary = $this->options->get_glossary( $post->ID ); if ( $glossary && ! empty( $glossary['active'] ) ) { return $this->render_glossary_page( $post->ID, $glossary, $content ); } // Check if this is a child of a glossary page. if ( $post->post_parent ) { $parent_glossary = $this->options->get_glossary( $post->post_parent ); if ( $parent_glossary && ! empty( $parent_glossary['active'] ) ) { return $this->render_entry_page( $post->post_parent, $parent_glossary, $content ); } } return $content; } /** * Render glossary page with A-Z navigation. * * @param int $page_id Glossary page ID. * @param array $glossary Glossary settings. * @param string $content Original content. * @return string Modified content. */ private function render_glossary_page( $page_id, $glossary, $content ) { $entries = azgl()->get_glossary_entries( $page_id ); if ( empty( $entries ) ) { $output = '
'; $output .= '

' . esc_html__( 'No glossary entries yet. Create child pages under this page to add entries.', '3task-glossary' ) . '

'; $output .= '
'; return $content . $output; } $alphabetic_list = $this->get_alphabetic_list( $entries ); $nav_style = $this->options->get( 'nav_style', 'buttons' ); $inactive_mode = $this->options->get( 'inactive_letters', 'accessible' ); $color_scheme = $this->options->get( 'color_scheme', 'emerald' ); // Migrate old boolean values. if ( true === $inactive_mode || '1' === $inactive_mode ) { $inactive_mode = 'show'; } elseif ( false === $inactive_mode || '' === $inactive_mode ) { $inactive_mode = 'none'; } $output = ''; // A-Z Navigation. if ( ! empty( $glossary['navigation'] ) ) { $output .= $this->render_navigation( $alphabetic_list, $nav_style, $color_scheme, $inactive_mode ); } // Entry list. $output .= '
'; foreach ( $alphabetic_list as $letter => $letter_entries ) { if ( empty( $letter_entries ) ) { continue; } $output .= '
'; $output .= '

' . esc_html( $letter ) . '

'; $output .= ''; $output .= '
'; } $output .= '
'; // Action buttons (Print) - only if enabled. if ( $this->options->get( 'enable_print', true ) ) { $output .= '
'; $output .= ''; $output .= '
'; } // Credit link (only if supporter mode enabled). $output .= $this->render_credit_link(); return $content . $output; } /** * Render entry page with navigation back to glossary. * * @param int $glossary_id Glossary page ID. * @param array $glossary Glossary settings. * @param string $content Original content. * @return string Modified content. */ private function render_entry_page( $glossary_id, $glossary, $content ) { global $post; $output = ''; // Add sub-navigation. if ( ! empty( $glossary['navigation'] ) ) { $entries = azgl()->get_glossary_entries( $glossary_id ); $alphabetic_list = $this->get_alphabetic_list( $entries ); $nav_style = $this->options->get( 'nav_style', 'buttons' ); $color_scheme = $this->options->get( 'color_scheme', 'emerald' ); $output .= $this->render_sub_navigation( $glossary_id, $alphabetic_list, $nav_style, $color_scheme ); } $after_content = ''; // Reverse Links (v2.3.0). $after_content .= $this->render_reverse_links( $post->ID ); return $output . $content . $after_content; } /** * Render reverse links section showing posts that link to this term. * * @since 2.3.0 * @param int $post_id Post ID. * @return string HTML. */ private function render_reverse_links( $post_id ) { global $wpdb; if ( ! AZGL_Content::public_page( $post_id ) ) { return ''; } $url = get_permalink( $post_id ); $needles = array_merge( array( $url, wp_make_link_relative( $url ), get_the_title( $post_id ) ), AZGL_Content::synonyms( $post_id ) ); $conditions = array(); $params = array( $post_id ); foreach ( $needles as $needle ) { if ( '' !== $needle ) { $conditions[] = 'post_content LIKE %s'; $params[] = '%' . $wpdb->esc_like( $needle ) . '%'; } } $sql = "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_password = '' AND post_type IN ('post','page') AND ID != %d AND (" . implode( ' OR ', $conditions ) . ') ORDER BY post_modified_gmt DESC, ID DESC LIMIT 100'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- Every value is prepared. $ids = $wpdb->get_col( $wpdb->prepare( $sql, $params ) ); $results = array(); foreach ( $ids as $id ) { $source = get_post( $id ); if ( ! AZGL_Content::public_page( $id ) ) { continue; } $manual = false; if ( class_exists( 'WP_HTML_Tag_Processor' ) ) { $tags = new WP_HTML_Tag_Processor( $source->post_content ); while ( $tags->next_tag( 'A' ) ) { $href = html_entity_decode( (string) $tags->get_attribute( 'href' ), ENT_QUOTES, 'UTF-8' ); if ( $href && '/' === $href[0] ) { $href = home_url( $href ); } if ( $href === $url || ( $href && url_to_postid( $href ) === (int) $post_id ) ) { $manual = true; break; } } } if ( $manual || azgl()->linker->links_to( $source, $post_id ) ) { $results[] = $source; } if ( count( $results ) >= 10 ) { break; } } if ( empty( $results ) ) { return ''; } $output = ''; return $output; } /** * Render A-Z navigation. * * @param array $alphabetic_list Alphabetic list of entries. * @param string $nav_style Navigation style. * @param string $color_scheme Color scheme. * @param string $inactive_mode Inactive letters display mode (accessible, hidden, show, none). * @return string HTML. */ private function render_navigation( $alphabetic_list, $nav_style, $color_scheme, $inactive_mode ) { $output = ''; return $output; } /** * Render an inactive letter based on accessibility mode. * * @param string $letter The letter to render. * @param string $inactive_mode Display mode (accessible, hidden, show, none). * @return string HTML. */ private function render_inactive_letter( $letter, $inactive_mode ) { switch ( $inactive_mode ) { case 'accessible': // Screen reader announces "X, no entries" - best for accessibility. return '' . esc_html( $letter ) . ''; case 'hidden': // Visible but hidden from screen readers. return ''; case 'show': // Legacy mode - visual only, may cause accessibility issues. return '' . esc_html( $letter ) . ''; case 'none': default: // Don't render inactive letters at all. return ''; } } /** * Render sub-navigation for entry pages. * * @param int $glossary_id Glossary page ID. * @param array $alphabetic_list Alphabetic list of entries. * @param string $nav_style Navigation style. * @param string $color_scheme Color scheme. * @return string HTML. */ private function render_sub_navigation( $glossary_id, $alphabetic_list, $nav_style, $color_scheme ) { $permalink = get_permalink( $glossary_id ); $output = ''; return $output; } /** * Get alphabetic list from entries. * * @param array $entries Glossary entries. * @return array Alphabetic list. */ private function get_alphabetic_list( $entries ) { // Initialize alphabet. $letters = array_merge( array( '#' ), range( 'A', 'Z' ) ); $list = array_fill_keys( $letters, array() ); // Sort entries by first letter. foreach ( $entries as $entry ) { $initial = $this->get_initial( $entry->post_title ); $list[ $initial ][] = array( 'id' => $entry->ID, 'title' => $entry->post_title, 'url' => get_permalink( $entry->ID ), ); } return $list; } /** * Get initial letter with umlaut handling. * * @param string $string Input string. * @return string Initial letter. */ private function get_initial( $string ) { $string = trim( $string ); if ( empty( $string ) ) { return '#'; } // German umlaut replacements. $replacements = array( 'Ä' => 'A', 'ä' => 'A', 'Ö' => 'O', 'ö' => 'O', 'Ü' => 'U', 'ü' => 'U', 'ß' => 'S', ); $first_char = mb_substr( $string, 0, 1, 'UTF-8' ); if ( isset( $replacements[ $first_char ] ) ) { return $replacements[ $first_char ]; } $first_char = strtoupper( $first_char ); if ( preg_match( '/^[A-Z]$/', $first_char ) ) { return $first_char; } return '#'; } /** * Render optional credit link. * * Only shown if user explicitly opts in via supporter mode. * * @return string HTML. */ private function render_credit_link() { if ( ! azgl()->is_supporter() ) { return ''; } $credit_url = $this->options->get( 'credit_link_url', 'https://www.3task.de' ); return '

' . '' . esc_html__( 'Glossary by', '3task-glossary' ) . ' ' . '3task' . '

'; } /** * Get color scheme CSS variables. * * @param string $scheme Color scheme name. * @param string $dark_mode Dark mode setting. * @return string CSS. */ private function get_color_scheme_css( $scheme, $dark_mode ) { $schemes = $this->get_color_schemes(); $colors = isset( $schemes[ $scheme ] ) ? $schemes[ $scheme ] : $schemes['emerald']; // Auto scheme: use theme colors if available. if ( 'auto' === $scheme ) { $css = ':root { --azgl-primary: var(--wp--preset--color--primary, #10b981); --azgl-hover: var(--wp--preset--color--secondary, #059669); --azgl-light: var(--wp--preset--color--tertiary, #d1fae5); --azgl-dark-text: var(--wp--preset--color--contrast, #065f46); }'; } else { $css = ':root { --azgl-primary: ' . $colors['primary'] . '; --azgl-hover: ' . $colors['hover'] . '; --azgl-light: ' . $colors['light'] . '; --azgl-dark-text: ' . $colors['dark-text'] . '; }'; } // Dark mode. $dark_css = ' --azgl-bg: #1e293b; --azgl-bg-inactive: #334155; --azgl-text: #f1f5f9; --azgl-text-muted: #94a3b8; --azgl-border: #475569; '; if ( 'dark' === $dark_mode ) { $css .= ':root {' . $dark_css . '}'; } elseif ( 'auto' === $dark_mode ) { $css .= '@media (prefers-color-scheme: dark) {:root {' . $dark_css . '}}'; } return $css; } /** * Get available color schemes. * * Lite: 2 schemes (Emerald, Slate) * Pro: 6 schemes + Custom colors * * @return array Color schemes. */ private function get_color_schemes() { return array( 'emerald' => array( 'primary' => '#10b981', 'hover' => '#059669', 'light' => '#d1fae5', 'dark-text' => '#065f46', ), 'slate' => array( 'primary' => '#64748b', 'hover' => '#475569', 'light' => '#f1f5f9', 'dark-text' => '#334155', ), ); } }