} */ public static function extract(\WP_Post $post): array { $fields = Settings::fields(); $title = in_array('title', $fields, true) || $post->post_type === 'attachment' ? self::clean(get_the_title($post)) : ''; $lead = []; if ($post->post_type === 'attachment') { $alt = self::clean((string) get_post_meta($post->ID, '_wp_attachment_image_alt', true)); if ($alt !== '') { $lead[] = 'Image: ' . $alt; } if (trim($post->post_excerpt) !== '') { $lead[] = 'Caption: ' . self::clean($post->post_excerpt); } } else { if (in_array('excerpt', $fields, true) && trim($post->post_excerpt) !== '') { $lead[] = 'Summary: ' . self::clean($post->post_excerpt); } if (in_array('meta_description', $fields, true)) { $key = Backfill::metaDescriptionKey(); $desc = $key !== '' ? self::clean((string) get_post_meta($post->ID, $key, true)) : ''; if ($desc !== '') { $lead[] = 'Description: ' . $desc; } } } if (in_array('terms', $fields, true)) { $names = self::termNames($post); if ($names !== []) { $lead[] = 'Topics: ' . implode(', ', $names); } } if (in_array('author', $fields, true) && $post->post_type !== 'attachment') { $author = self::clean((string) get_the_author_meta('display_name', (int) $post->post_author)); if ($author !== '') { $lead[] = 'Author: ' . $author; } } if (in_array('custom_fields', $fields, true)) { foreach (Settings::metaKeys() as $key) { $value = self::metaText(get_post_meta($post->ID, $key, false)); if ($value !== '') { $lead[] = self::label($key) . ': ' . $value; } } } $content = in_array('content', $fields, true) || $post->post_type === 'attachment' ? self::contentToText((string) $post->post_content) : ''; $body = trim(implode("\n", $lead) . ($lead !== [] && $content !== '' ? "\n\n" : '') . $content); /** * Filters the text embedded for a post, before chunking. Headings are "## " lines. * * @param string $body The text. * @param \WP_Post $post The post or attachment. */ $body = (string) apply_filters('aipcf_embed_text', $body, $post); return ['title' => $title, 'body' => $body, 'metadata' => self::metadata($post)]; } /** * Words in a text: runs of letters and digits, with the marks that belong to them, so * Marathi and Hindi (whose vowel signs are marks) count like English. Scripts written * without spaces (Chinese, Japanese, Thai) count each run as one word. */ public static function words(string $text): int { return (int) preg_match_all('/[\p{L}\p{N}][\p{L}\p{N}\p{M}\'\x{2019}-]*/u', $text); } /** * Saved post content to plain text with "## " headings and blank-line paragraphs. */ public static function contentToText(string $html): string { $html = (string) preg_replace('//s', '', $html); // block delimiters, comments $html = (string) preg_replace('#<(script|style|noscript|svg|iframe)\b.*?#is', '', $html); $html = (string) preg_replace('/\[\/?[A-Za-z][\w-]*(?:\s[^\]]*)?\]/', ' ', $html); // shortcode tags, keep inner text $html = (string) preg_replace_callback('##is', [self::class, 'tableToText'], $html); $html = (string) preg_replace_callback( '#]*>(.*?)#is', static fn(array $m): string => "\n\n## " . trim(self::clean($m[1])) . "\n\n", $html ); $html = (string) preg_replace('#<(br|hr)\b[^>]*>#i', "\n", $html); $html = (string) preg_replace('#]*>#i', "\n\n", $html); $html = (string) preg_replace('#]*>#i', "\n- ", $html); $text = html_entity_decode(wp_strip_all_tags($html, false), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $text = str_replace("\xC2\xA0", ' ', $text); $text = (string) preg_replace('/[ \t]+/', ' ', $text); $text = (string) preg_replace('/ *\n */', "\n", $text); $text = (string) preg_replace('/\n{3,}/', "\n\n", $text); $text = (string) preg_replace("/^## *\n/m", '', $text); // empty headings return trim($text); } /** * A table as one line per row, cells separated by " | ", whatever whitespace the HTML has. * * @param array $m */ private static function tableToText(array $m): string { $rows = []; foreach (preg_split('#]*>#i', $m[0]) ?: [] as $row) { $cells = []; foreach (preg_split('##i', $row) ?: [] as $cell) { $cell = self::clean($cell); if ($cell !== '') { $cells[] = $cell; } } if ($cells !== []) { $rows[] = implode(' | ', $cells); } } return "\n\n" . implode("\n", $rows) . "\n\n"; } /** * Stored with every chunk of the post. Keys with metadata indexes in the store * (site, post_type, object_id, lang) are filterable. * * @return array */ public static function metadata(\WP_Post $post): array { $metadata = [ 'site' => self::siteKey(), 'object_type' => $post->post_type === 'attachment' ? 'attachment' : 'post', 'object_id' => (int) $post->ID, 'post_type' => $post->post_type, 'title' => mb_substr(self::clean(get_the_title($post)), 0, 200), 'url' => $post->post_type === 'attachment' ? (string) wp_get_attachment_url($post->ID) : (string) get_permalink($post), 'date' => (string) $post->post_date_gmt, // No modified time: it changes on every save and is part of the content // hash, so a save without edits would be embedded (and billed) again. 'lang' => self::lang($post), ]; /** * Filters the metadata stored with each chunk of a post (text is added per chunk). * * @param array $metadata * @param \WP_Post $post */ return (array) apply_filters('aipcf_embed_metadata', $metadata, $post); } /** * Short, stable ID for this site, so one index can hold several sites. */ public static function siteKey(): string { return substr(md5(home_url() . '|' . get_current_blog_id()), 0, 8); } private static function lang(\WP_Post $post): string { $locale = (string) apply_filters('aipcf_embed_locale', get_locale(), $post); return strtolower((string) strtok($locale, '_-')); } /** * @return list */ private static function termNames(\WP_Post $post): array { $names = []; foreach (get_object_taxonomies($post->post_type, 'objects') as $taxonomy) { if (!$taxonomy->public) { continue; } $terms = get_the_terms($post, $taxonomy->name); if (is_array($terms)) { foreach ($terms as $term) { if ($term->slug !== 'uncategorized') { $names[] = self::clean($term->name); } } } } return array_values(array_unique(array_filter($names))); } /** * @param array $values */ private static function metaText(array $values): string { $out = []; array_walk_recursive($values, static function ($v) use (&$out): void { if (is_scalar($v) && !is_bool($v)) { $t = self::clean((string) $v); // Skip empty values and long numbers (IDs, timestamps), which carry no meaning. if ($t !== '' && !(is_numeric($t) && strlen($t) >= 10)) { $out[] = $t; } } }); return mb_substr(implode(', ', array_unique($out)), 0, 2000); } private static function label(string $metaKey): string { return ucfirst(trim(str_replace(['_', '-'], ' ', $metaKey))); } private static function clean(string $text): string { $text = html_entity_decode(wp_strip_all_tags($text), ENT_QUOTES | ENT_HTML5, 'UTF-8'); return trim((string) preg_replace('/\s+/u', ' ', str_replace("\xC2\xA0", ' ', $text))); } }