validator = new Schema_AI_Schema_Validator(); } /** * Generate complete schema markup for a post. * * This is the main entry point. It: * 1. Resolves the correct type builder via SchemaFactory. * 2. Extracts fields from the AI analysis (detected_entities). * 3. Builds the schema via the type builder. * 4. Adds common fields (@context, url, name/headline, dates). * 5. Validates the result. * 6. Returns the schema, validation result, and confidence score. * * @param int $post_id The WordPress post ID. * @param string $schema_type The schema.org type to generate (e.g. 'Article'). * @param array|null $ai_analysis Optional AI analysis result containing * detected_entities and confidence. * @return array { * @type array $schema The complete schema array. * @type array $validation Validation result (valid, errors, warnings, score). * @type float|null $confidence AI confidence score (0-1) or null. * } * @throws \Exception If the schema type is unsupported. */ public function generate( $post_id, $schema_type, $ai_analysis = null ) { // Get the schema builder for the requested type. $builder = Schema_AI_Schema_Factory::get_builder( $schema_type ); if ( ! $builder ) { throw new \Exception( sprintf( /* translators: %s: schema type name */ esc_html__( 'Unsupported schema type: %s', 'ai-schema-gen' ), esc_html( $schema_type ) ) ); } // Extract fields from the AI analysis. $entities = array(); if ( is_array( $ai_analysis ) && isset( $ai_analysis['detected_entities'] ) ) { $entities = $ai_analysis['detected_entities']; } // Use the FieldExtractor if available, otherwise pass entities directly. $fields = $this->extract_fields( $post_id, $schema_type, $entities ); // Build the schema through the type-specific builder. $schema = $builder->build( $post_id, $fields ); // Override @type for aliases: BlogPosting/NewsArticle share the // Article builder, AboutPage/ContactPage share the WebPage builder. // Each builder returns its own default @type; the requested type is // the one that actually belongs in the output. if ( isset( $schema['@type'] ) && $schema['@type'] !== $schema_type ) { $aliases = array( 'BlogPosting', 'NewsArticle', 'AboutPage', 'ContactPage' ); if ( in_array( $schema_type, $aliases, true ) ) { $schema['@type'] = $schema_type; } } // Add common fields shared by all schema types. $schema = $this->add_common_fields( $schema, $post_id ); // Validate. $validation = $this->validator->validate( $schema ); return array( 'schema' => $schema, 'validation' => $validation, 'confidence' => isset( $ai_analysis['confidence'] ) ? $ai_analysis['confidence'] : null, ); } /** * Extract fields for the schema builder. * * If the FieldExtractor class is available (from the ai/ directory), * delegates to it. Otherwise, returns the AI entities directly as * the field array for the builder. * * @param int $post_id The WordPress post ID. * @param string $schema_type The schema type. * @param array $entities AI-detected entities. * @return array The extracted fields. */ private function extract_fields( $post_id, $schema_type, $entities ) { // Try to use the full FieldExtractor if it exists. $extractor_file = dirname( dirname( __FILE__ ) ) . '/ai/class-field-extractor.php'; if ( file_exists( $extractor_file ) ) { if ( ! class_exists( 'Schema_AI_Field_Extractor' ) ) { require_once $extractor_file; } if ( class_exists( 'Schema_AI_Field_Extractor' ) ) { $extractor = new Schema_AI_Field_Extractor(); if ( method_exists( $extractor, 'extract_fields' ) ) { return $extractor->extract_fields( $post_id, $schema_type, $entities ); } } } // Fallback: return the AI entities directly as fields. return is_array( $entities ) ? $entities : array(); } /** * Add common fields to all schema types. * * Ensures @context, url, name/headline, datePublished, and dateModified * are present in every schema. Does not overwrite values that were already * set by the type builder. * * @param array $schema The schema array from the builder. * @param int $post_id The WordPress post ID. * @return array The schema with common fields added. */ private function add_common_fields( $schema, $post_id ) { $post = get_post( $post_id ); if ( ! $post ) { return $schema; } // @context. if ( ! isset( $schema['@context'] ) ) { $schema = array_merge( array( '@context' => 'https://schema.org' ), $schema ); } // URL. if ( ! isset( $schema['url'] ) ) { $schema['url'] = get_permalink( $post_id ); } // Name or headline. if ( ! isset( $schema['name'] ) && ! isset( $schema['headline'] ) ) { $headline_types = array( 'Article', 'BlogPosting', 'NewsArticle' ); $type = isset( $schema['@type'] ) ? $schema['@type'] : ''; $key = in_array( $type, $headline_types, true ) ? 'headline' : 'name'; $schema[ $key ] = $post->post_title; } // Dates. if ( ! isset( $schema['datePublished'] ) ) { $schema['datePublished'] = get_the_date( 'c', $post_id ); } if ( ! isset( $schema['dateModified'] ) ) { $schema['dateModified'] = get_the_modified_date( 'c', $post_id ); } return $schema; } /** * Calculate a quality score from 0-100. * * Deducts 20 points per error and 5 points per warning from 100. * * @param array $errors List of validation errors. * @param array $warnings List of validation warnings. * @return int Score clamped between 0 and 100. */ public function calculate_score( $errors, $warnings ) { $score = 100; $score -= count( $errors ) * 20; $score -= count( $warnings ) * 5; return max( 0, min( 100, $score ) ); } }