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 = '';
$output .= '
' . esc_html__( 'This term is mentioned in:', '3task-glossary' ) . '
';
$output .= '
';
foreach ( $results as $result ) {
$type_label = 'page' === $result->post_type ? __( 'Page', '3task-glossary' ) : __( 'Post', '3task-glossary' );
$output .= '- ';
$output .= '' . esc_html( $result->post_title ) . '';
$output .= '' . esc_html( $type_label ) . '';
$output .= '
';
}
$output .= '
';
$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 '' .
esc_html( $letter ) . '';
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',
),
);
}
}