]*>.*?<\/a>)(\s*)$/isu'; /** * Where one sentence ends and the next begins. * * A full stop, ?, ! or Bangla danda (ред), followed by a space. * The space rule keeps "9.30" and "wp-admin.php" in one piece. * Speech_Chunker uses the same rule. * * @var string */ private const SENTENCE_END = '/(?<=[.!?\x{0964}])(?=\s)/u'; /** * Words in setting names that say nothing about the setting. * * Many Eventin field names start with "Enable", "Select" or "Settings". * These words are ignored, so a guess like "Select a date" does not match * a field called "Select Currency". * * @var string[] */ private const COMMON_WORDS = [ 'enable', 'disable', 'select', 'settings', 'setting', 'show', 'hide', 'allow', 'choose', 'default', 'page', 'pages', 'option', 'options', 'with', 'your', 'from', 'this', 'that' ]; /** * Drop the invented steps from an answer that opens a page. * * @param string $text The whole answer, or the part of it that has * not been spoken yet. * @param int $tool_calls How many tools this turn asked for. * @param bool $opening_said True when the first sentence has already * gone out to the caller as its own clip, so * what is left here is the steps and the link. * @param array $known Names of the settings on the page the link * opens. A sentence naming one may stay. * @return string The answer, unchanged unless a turn that looked nothing * up wrote sentences between its opening line and its link. */ public static function fix( string $text, int $tool_calls, bool $opening_said = false, array $known = [] ): string { // Something was looked up, so the later sentences may be real. if ( 0 !== $tool_calls ) { return $text; } $tag = ''; $body = $text; if ( preg_match( Text_To_Speech::LEADING_TONE_TAG, $text, $m ) ) { $tag = $m[1]; $body = (string) substr( $text, strlen( $m[1] ) ); } // No page is opening, so there is no screen to invent steps for. if ( ! preg_match( self::TRAILING_LINK, $body, $parts ) ) { return $text; } $prose = $parts[1]; $gap = $parts[2]; $link = $parts[3] . $parts[4]; if ( '' === trim( $prose ) ) { return $text; } $sentences = array_values( array_filter( array_map( 'trim', (array) preg_split( self::SENTENCE_END, $prose ) ), static fn( $piece ) => '' !== $piece ) ); // One sentence in front of the link is already the shape we want. $keep = $opening_said ? [] : array_slice( $sentences, 0, 1 ); $next = $sentences[ count( $keep ) ] ?? ''; // Plus one how-to sentence, when it names a setting on that page. if ( '' !== $next && self::names_one_of( $next, $known ) ) { $keep[] = $next; } if ( count( $sentences ) <= count( $keep ) ) { return $text; } if ( empty( $keep ) ) { return $tag . $link; } return $tag . implode( ' ', $keep ) . $gap . $link; } /** * Check if this sentence names one of these settings. * * One word is enough: "Select Currency" matches a sentence with "currency". * Words shorter than 4 letters and COMMON_WORDS are ignored. * * @param string $sentence One sentence of the answer. * @param array $known Setting names from the page. * @return bool */ private static function names_one_of( string $sentence, array $known ): bool { foreach ( $known as $name ) { foreach ( preg_split( '/[^\p{L}\p{N}]+/u', (string) $name ) as $word ) { if ( mb_strlen( $word ) >= 4 && ! in_array( mb_strtolower( $word ), self::COMMON_WORDS, true ) && false !== mb_stripos( $sentence, $word ) ) { return true; } } } return false; } }