ここの記事は全文ではなく冒頭の何文字かを抜粋してマストドンで表示させているのだが、その抜粋文字数が極端に少ないことがある。その原因は抜粋するための関数にあるようなので、フィルターフックを使って、その関数の結果とは異なる結果になるように対処した。
まず[ap_excerpt]というショートコードを生成するコードは /activitypub/includes/class-shortcodes.php の中にある。
/**
* Generates output for the 'ap_excerpt' Shortcode
*
* @param array $attributes The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post excerpt.
*/
public static function excerpt( $attributes, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$attributes = shortcode_atts(
array( 'length' => ACTIVITYPUB_EXCERPT_LENGTH ),
$attributes,
$tag
);
$excerpt_length = intval( $attributes['length'] );
if ( 0 === $excerpt_length ) {
$excerpt_length = ACTIVITYPUB_EXCERPT_LENGTH;
}
$excerpt = generate_post_summary( $item, $excerpt_length );
/** This filter is documented in wp-includes/post-template.php */
return \apply_filters( 'the_excerpt', $excerpt );
}
実際に処理している関数は次の部分。
$excerpt = generate_post_summary( $item, $excerpt_length );
この後に、次のフィルターがあるので、ここに結果を修正する余地がある。
return \apply_filters( 'the_excerpt', $excerpt );
実際に処理しているgenerate_post_summary()関数は、/activitypub/includes/functions.php の中にある。
/**
* Generate a summary of a post.
*
* This function generates a summary of a post by extracting:
*
* 1. The post excerpt if it exists.
* 2. The first part of the post content if it contains the <!--more--> tag.
* 3. An excerpt of the post content if it is longer than the specified length.
*
* @param int|\WP_Post $post The post ID or post object.
* @param integer $length The maximum length of the summary.
* Default is 500. It will be ignored if the post excerpt
* and the content above the <!--more--> tag.
*
* @return string The generated post summary.
*/
function generate_post_summary( $post, $length = 500 ) {
$post = get_post( $post );
if ( ! $post ) {
return '';
}
/**
* Filters the excerpt more value.
*
* @param string $excerpt_more The excerpt more.
*/
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[…]' );
$length = $length - \mb_strlen( $excerpt_more, 'UTF-8' );
$content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID );
if ( $content ) {
// Ignore length if excerpt is set.
$length = null;
} else {
$content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID );
$content_parts = \get_extended( $content );
// Check for the <!--more--> tag.
if (
! empty( $content_parts['extended'] ) &&
! empty( $content_parts['main'] )
) {
$content = \trim( $content_parts['main'] ) . ' ' . $excerpt_more;
$length = null;
}
}
$content = \strip_shortcodes( $content );
$content = \wp_strip_all_tags( $content );
$content = \html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
$content = \trim( $content );
$content = \preg_replace( '/\R+/mu', "\n\n", $content );
$content = \preg_replace( '/[\r\t]/u', '', $content );
if ( $length && \mb_strlen( $content, 'UTF-8' ) > $length ) {
$content = \wordwrap( $content, $length, '</activitypub-summary>' );
$content = \explode( '</activitypub-summary>', $content, 2 );
$content = $content[0] . ' ' . $excerpt_more;
}
/*
There is no proper support for HTML in ActivityPub summaries yet.
// This filter is documented in wp-includes/post-template.php.
return \apply_filters( 'the_excerpt', $content );
*/
return $content;
}
このコードは一見、マルチバイトの文字に対処して日本語の1文字は1文字とカウントしているように見える。しかし、この中の wordwrap はマルチバイトに対応していない。バイト数で数えるらしい。英数字は1文字1バイトなので問題が生じないが、日本語の1文字は通常は3バイトなので、日本語以外も含めてマルチバイトの処理で異常が生じる。500バイトでは、166文字しか抜粋しない。実際は $excerpt_more を引いているのでさらに小さい。また、wordwrap の仕様を考慮すると、さらに少ない文字数しか抜粋しない。パラメータの cut_long_words を true に設定すると、日本語の1文字を分割してしまって、コードが異常になり、文字化けが生じかねない。実際にはデフォルトの false になっているらしい。
そこで、テーマファイルエディターの functions.php にコードを追加して、フィルターフックを利用して、できるだけ希望通りに抜粋されるようにした。その結果の一例は次の画像の通りである。
何もしない状態の抜粋の様子。

フィルターフックを利用して300文字弱を抜粋できるようにした結果。

さて、テーマファイルエディターの functions.php に追加したコードは次のとおりである。Geminiに相談しながら作成した。
// PHPの内部エンコーディングをUTF-8に設定 (文字化け対策)
mb_internal_encoding("UTF-8");
mb_regex_encoding("UTF-8");
// =========================================================================
// 0. FIXED LENGTH AND ASCII EXCERPT MORE DEFINITION
// =========================================================================
// 抜粋長を300文字に固定
define('FIXED_EXCERPT_LENGTH', 300);
// カスタム省略記号(念のためにASCIIのみで確認した後に日本語に)
define('CUSTOM_EXCERPT_MORE_ASCII', ' [...続きは下のURLで]');
// =========================================================================
// 1. URL COMPENSATED TRIM FUNCTION (Multi-byte safe & URL 25文字補正)
// =========================================================================
function custom_mb_trim_url_compensated($string, $length, $excerpt_more) {
if (\mb_strlen($string, 'UTF-8') <= $length) {
return $string;
}
$target_length = $length;
$compensated_content = '';
$current_pos = 0;
$url_pattern = '#(https?:\/\/[\x21-\x7E]+)#iu';
$parts = \preg_split($url_pattern, $string, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
foreach ($parts as $part) {
if ($current_pos >= $target_length) {
break;
}
if (\preg_match($url_pattern, $part)) {
$compensated_len = 25;
if (($current_pos + $compensated_len) <= $target_length) {
$compensated_content .= $part;
$current_pos += $compensated_len;
} else {
break;
}
} else {
$remaining_count = $target_length - $current_pos;
$text_part = \mb_substr($part, 0, $remaining_count, 'UTF-8');
$compensated_content .= $text_part;
$current_pos += \mb_strlen($text_part, 'UTF-8');
}
}
return \trim($compensated_content) . $excerpt_more;
}
// =========================================================================
// 2. THE EXCERPT FILTER (優先度0: 元のコンテンツを取得し、プラグインの処理を迂回)
// =========================================================================
// 優先度0で、ActivityPubプラグインより先に実行されます。
// $post->post_content (記事の生の内容) からタグやエンティティをデコードしたものを返すことで、
// プラグインによる途中の切断を防ぎます。
add_filter('the_excerpt', 'activitypub_get_full_content_for_trimming', 0);
function activitypub_get_full_content_for_trimming($excerpt) {
global $post;
// $postオブジェクトが利用可能な場合
if ($post instanceof WP_Post) {
$full_content = $post->post_content;
} else {
// 利用できない場合は、元の抜粋をそのまま返す
return $excerpt;
}
// ActivityPubプラグインの初期処理を模倣(タグ除去とエンティティデコード)
// これにより、ハッシュタグやウムラウトの問題を回避しつつ、生のコンテンツを取得します。
$content_to_trim = strip_tags($full_content);
$content_to_trim = html_entity_decode($content_to_trim, ENT_QUOTES, 'UTF-8');
// 優先度1のフックに渡す「生のコンテンツ」として返します。
return $content_to_trim;
}
// =========================================================================
// 3. THE EXCERPT FILTER (優先度1: 再トリミングの実行)
// =========================================================================
// 優先度1で、プラグインの処理より後に実行されます。
// 優先度0で受け取った「生のコンテンツ」を、カスタムロジックで再トリミングします。
add_filter('the_excerpt', 'activitypub_fix_mb_excerpt_final', 1);
function activitypub_fix_mb_excerpt_final($excerpt) {
// 抜粋長と省略記号を設定
$length = FIXED_EXCERPT_LENGTH;
$custom_excerpt_more = CUSTOM_EXCERPT_MORE_ASCII;
$effective_length = $length - \mb_strlen($custom_excerpt_more, 'UTF-8');
$content_to_trim = \trim($excerpt); // 優先度0で渡された「生のコンテンツ」がここに入ります
// 抜粋が固定値の長さを超えている場合、強制的に再トリミング
if (\mb_strlen($content_to_trim, 'UTF-8') > $effective_length) {
return custom_mb_trim_url_compensated($content_to_trim, $effective_length, $custom_excerpt_more);
}
// 短いコンテンツやトリミング不要な場合は、そのまま返す
return $excerpt;
}


コメント