$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;
}
}