$token['text'], 'sentence' => mb_substr( $sentence, 0, 160 ), ); if ( count( $found ) >= self::MAX_NUMBERS ) { break 2; } } } return array_values( $found ); } /** * Article body as plain text, with block boundaries kept as line breaks. * * @param string $html Article body. * @return string */ private static function plain_text( $html ) { $html = (string) preg_replace( '//su', "\n", $html ); $html = (string) preg_replace( '/<\/(p|h[1-6]|li|td|th|tr|div|blockquote|figcaption)>|/iu', "$0\n", $html ); return html_entity_decode( wp_strip_all_tags( $html ), ENT_QUOTES, 'UTF-8' ); } /** * Split text into sentences. * * @param string $text Plain text. * @return array */ private static function sentences( $text ) { $parts = preg_split( '/(?<=[。!?!?])|(?<=\.)\s+|\n+/u', $text ); $out = array(); foreach ( (array) $parts as $part ) { $part = trim( (string) $part ); if ( '' !== $part ) { $out[] = $part; } } return $out; } /** * Whether a sentence is written in the first person. * * @param string $sentence One sentence. * @return bool */ public static function is_first_person( $sentence ) { // Japanese first person words. "自分" and "うち" are left out because // they are just as often used for the reader or in other senses. if ( preg_match( '/私|僕|ぼく|わたし|わたくし|筆者|俺|我が家|わが家/u', $sentence ) ) { return true; } // English. A capital I on its own, or one of the other first person forms. if ( preg_match( "/\\bI\\b|\\bI['’](?:m|ve|d|ll)\\b/u", $sentence ) ) { return true; } return (bool) preg_match( '/\b(?:my|me|mine|myself|we|our|ours|us|ourselves)\b/iu', $sentence ); } /** * Lookup keys for every number in the author's experience: the bare value, * and the value with its unit when it has one. * * @param string $text Experience typed by the author. * @return array Keys as array keys. */ private static function supplied_keys( $text ) { $keys = array(); foreach ( self::number_tokens( self::normalize( $text ) ) as $token ) { foreach ( $token['values'] as $value ) { $keys[ $value ] = true; if ( '' !== $token['unit'] ) { $keys[ $value . '|' . $token['unit'] ] = true; } } } return $keys; } /** * Whether every value of a token appears in the experience, with the * same unit when the token has one. * * @param array $token Token from number_tokens(). * @param array $supplied Keys from supplied_keys(). * @return bool */ private static function is_supplied( $token, $supplied ) { foreach ( $token['values'] as $value ) { $key = '' !== $token['unit'] ? $value . '|' . $token['unit'] : $value; if ( ! isset( $supplied[ $key ] ) ) { return false; } } return true; } /** * Numbers, ranges and the unit that follows them. * * "85〜95℃" gives one token with the values 85 and 95 and the unit "°C". * "30g/30秒" gives two tokens. * * @param string $text Normalised text. * @return array List of arrays with the keys "text", "values" and "unit". */ private static function number_tokens( $text ) { $number = '\d+(?:,\d{3})*(?:\.\d+)?'; $range = '(?:\s*(?:〜|~|~|-|–|—|to|から)\s*' . $number . ')?'; // Japanese units and counters are listed rather than matched as "any // kanji", so that the verb after a number is not read as its unit. $japanese = '分間|時間|日間|週間|か月|ヶ月|ヵ月|カ月|年間|秒|分|日|週|月|年|回|杯|個|枚|本|人|円|度|倍|割|件|歳|才|冊|台|軒|粒|滴|合|キロ|グラム|ミリ|リットル|センチ|メートル|カップ|ワット|ページ'; $unit = '(?:\s?(%|°C|°F|°|' . $japanese . '|[A-Za-zµ]{1,4}(?![A-Za-z])))?'; if ( ! preg_match_all( '/(? trim( $match[0] ), 'values' => array_values( array_unique( $values ) ), 'unit' => isset( $match[2] ) ? mb_strtolower( $match[2], 'UTF-8' ) : '', ); } return $tokens; } /** * "1,000" and "1000.0" both become "1000". * * @param string $digits Number as written. * @return string */ private static function canonical_number( $digits ) { $digits = str_replace( ',', '', $digits ); if ( false !== strpos( $digits, '.' ) ) { $digits = rtrim( rtrim( $digits, '0' ), '.' ); } $digits = ltrim( $digits, '0' ); return '' === $digits || '.' === $digits[0] ? '0' . $digits : $digits; } // ----------------------------------------------------------------- // Categories. // ----------------------------------------------------------------- /** * Names of the categories that already exist, most used first. * * @param int $limit How many to return. * @return array */ public static function existing_category_names( $limit = 40 ) { $terms = get_terms( array( 'taxonomy' => 'category', 'hide_empty' => false, 'orderby' => 'count', 'order' => 'DESC', 'number' => max( 1, (int) $limit ), ) ); if ( is_wp_error( $terms ) ) { return array(); } $names = array(); foreach ( $terms as $term ) { $names[] = html_entity_decode( (string) $term->name, ENT_QUOTES, 'UTF-8' ); } return $names; } /** * Find an existing term for a suggested name. * * The comparison ignores surrounding space, letter case and the difference * between full width and half width characters (Unicode NFKC), and also * accepts a name that matches a term slug. * * @param string $name Suggested name. * @param string $taxonomy Taxonomy. * @return int Term id, or 0 when nothing matches. */ public static function match_term( $name, $taxonomy ) { $key = self::term_key( $name ); if ( '' === $key ) { return 0; } $slug = sanitize_title( $name ); $terms = get_terms( array( 'taxonomy' => $taxonomy, 'hide_empty' => false, ) ); if ( is_wp_error( $terms ) ) { return 0; } foreach ( $terms as $term ) { if ( self::term_key( html_entity_decode( (string) $term->name, ENT_QUOTES, 'UTF-8' ) ) === $key ) { return (int) $term->term_id; } } foreach ( $terms as $term ) { $term_slug = rawurldecode( (string) $term->slug ); if ( ( '' !== $slug && rawurldecode( $slug ) === $term_slug ) || self::term_key( $term_slug ) === $key ) { return (int) $term->term_id; } } return 0; } /** * Comparison key for a term name. * * @param string $name Term name. * @return string */ private static function term_key( $name ) { $name = self::normalize( trim( (string) $name ) ); $name = (string) preg_replace( '/\s+/u', ' ', $name ); return mb_strtolower( trim( $name ), 'UTF-8' ); } // ----------------------------------------------------------------- // Internal link anchors. // ----------------------------------------------------------------- /** * Put the real post title back on internal links whose anchor text was * translated. * * For every link that resolves to a post on this site, the anchor text and * the post title are compared. When one contains Japanese script and the * other does not, the anchor was translated, so it is replaced with the * title as it is. Links whose anchor contains other HTML are left alone. * * @param string $html Article body. * @return string */ public static function fix_internal_anchors( $html ) { $fixed = preg_replace_callback( '/]*?)\shref=(["\'])(.*?)\2([^>]*)>(.*?)<\/a>/isu', function ( $matches ) { $inner = $matches[5]; if ( '' === trim( $inner ) || false !== strpos( $inner, '<' ) ) { return $matches[0]; } $post_id = url_to_postid( html_entity_decode( $matches[3], ENT_QUOTES, 'UTF-8' ) ); if ( ! $post_id ) { return $matches[0]; } $title = html_entity_decode( (string) get_the_title( $post_id ), ENT_QUOTES, 'UTF-8' ); $anchor = html_entity_decode( $inner, ENT_QUOTES, 'UTF-8' ); if ( '' === trim( $title ) || ACSAGEN_Checks::has_japanese( $anchor ) === ACSAGEN_Checks::has_japanese( $title ) ) { return $matches[0]; } return '' . esc_html( $title ) . ''; }, (string) $html ); return null === $fixed ? (string) $html : $fixed; } /** * Whether a string contains hiragana, katakana or kanji. * * @param string $text Any text. * @return bool */ public static function has_japanese( $text ) { return (bool) preg_match( '/[\x{3040}-\x{30FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FFF}\x{FF66}-\x{FF9F}]/u', (string) $text ); } // ----------------------------------------------------------------- // Helpers. // ----------------------------------------------------------------- /** * Unicode NFKC normalisation when the intl extension is available, with a * fallback that at least folds full width ASCII. * * @param string $text Any text. * @return string */ private static function normalize( $text ) { $text = (string) $text; if ( class_exists( 'Normalizer' ) ) { $normalized = Normalizer::normalize( $text, Normalizer::FORM_KC ); if ( is_string( $normalized ) ) { return $normalized; } } if ( function_exists( 'mb_convert_kana' ) ) { return mb_convert_kana( $text, 'asKV', 'UTF-8' ); } return $text; } }