プラグイン「ActivityPub」の動作を分析している時に、ちょっと気になることがあった。
見ていたのは、wp_posts テーブルで post_type が ap_outbox になっているデータの post_content であるが、UTF-8にエスケープされていて読みにくいのでGeminiにデコードしてもらって確認した。チェックしたのは改行部分。見ると、<br>ではなく<br />になっている。
調べてみたら、どうやら、content を作成する際に、wpautop()関数を使っていることが原因らしい。
A group of regex replaces used to identify text formatted with newlines and replace double line breaks with HTML paragraph tags. The remaining line breaks after conversion become
wpautop() – Function | Developer.WordPress.org<br />tags, unless$bris set to ‘0’ or ‘false’.
さて、調べた順に書いていく。
まず、wp_posts テーブルで post_type が ap_outbox になっているデータの一つ、気になっていた投稿のデータの post_content を確認した。そのデータの content の部分は次のようになっていた。
"content":"\u003Cp\u003E\u003Cstrong\u003EWordPress\u306e\u629c\u7c8b\u3067\u6539\u884c\u304c\u524a\u9664\u3055\u308c\u308b\u539f\u56e0\u306f wp_posts \u30c6\u30fc\u30d6\u30eb\u306e post_content\u003C/strong\u003E\u003Cbr /\u003E\u003Ca rel=\u0022tag\u0022 class=\u0022hashtag u-tag u-category\u0022 href=\u0022https://ishii00141.stars.ne.jp/tag/gemini/\u0022\u003E#Gemini\u003C/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022hashtag u-tag u-category\u0022 href=\u0022https://ishii00141.stars.ne.jp/tag/php/\u0022\u003E#PHP\u003C/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022hashtag u-tag u-category\u0022 href=\u0022https://ishii00141.stars.ne.jp/tag/wordpress/\u0022\u003E#WordPress\u003C/a\u003E \u003Ca rel=\u0022tag\u0022 class=\u0022hashtag u-tag u-category\u0022 href=\u0022https://ishii00141.stars.ne.jp/tag/%e4%bb%95%e6%a7%98/\u0022\u003E#\u4ed5\u69d8\u003C/a\u003E\u003C/p\u003E\u003Cp\u003E\u3000WordPress\u306e\u5165\u529b\u6642\u306bEnter\u30ad\u30fc\u3092\u53e9\u304f\u3068\u6bb5\u843d\u304c\u5909\u308f\u308b\u306e\u3060\u304c\u3001\u5358\u306b\u6539\u884c\u3057\u305f\u3044\u3053\u3068\u3082\u3042\u308a\u3001\u305d\u306e\u5834\u5408\u306fShift\u30ad\u30fc\u3092\u62bc\u3057\u306a\u304c\u3089Enter\u30ad\u30fc\u3092\u53e9\u304f\u3002\u003Cbr /\u003E\u3000\u305d\u3046\u3059\u308b\u3068\u3001\u6bb5\u843d\u304c\u5909\u308f\u3089\u305a\u306b\u6539\u884c\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u308b\u3002\u003Cbr /\u003E\u3000\u305d\u3082\u305d\u3082\u3001\u300c\u6bb5\u843d\u300d\u3068\u3044\u3046\u8868\u73fe\u304c\u65e5\u672c\u8a9e\u3068\u7570\u306a\u3063\u3066\u3044\u308b\u304b\u3089\u56f0\u308b\u3002\u6539\u884c\u3057\u3066\u4e00\u6587\u5b57\u5b57\u4e0b\u3052\u3059\u308c\u3070\u3001\u305d\u308c\u306f\u65b0\u3057\u3044\u6bb5\u843d\u306a\u306e\u306b\u3001html\u306e\u4e16\u754c\u3067\u306f\u305d\u3046\u306f\u307f\u306a\u3055\u306a\u3044\u3002\u003C/p\u003E\u003Cp\u003E\u3000\u305d\u308c\u306f\u3068\u3082\u304b\u304f\u3001WordPress\u306e\u629c\u7c8b\u30d7\u30ed\u30b0\u30e9\u30e0\u3067\u306fhtml\u30bf\u30b0\u3092\u524a\u9664\u3059\u308b\u306e\u3060\u304c\u3001\u6b21\u306e\u3088\u3046\u306a\u30b3\u30fc\u30c9\u306e\u5834\u5408\u3001Web\u8868\u793a\u3067\u306f\u6539\u884c\u304c\u3042\u308b\u306e\u306b\u3001\u30bf\u30b0\u3092\u524a\u9664\u3057\u3061\u3083\u3046\u3068\u6539\u884c\u304c\u7121\u304f\u306a\u308b\u3002\u003C/p\u003E\u003Cp\u003E\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u003Cbr /\u003EAccount h [...\u7d9a\u304d\u306f\u4e0b\u306eURL\u3067]\u003C/p\u003E\u003Cp\u003E\u003Ca href=\u0022https://ishii00141.stars.ne.jp/20251203-1656-4312/\u0022\u003Ehttps://ishii00141.stars.ne.jp/20251203-1656-4312/\u003C/a\u003E\u003C/p\u003E",
これをMastodonなどが整形して表示したわけだが、このままでは分かりにくいので Unicode エスケープ(UTF-8 相当)をデコードすると、次のようになる。
ただし、読みやすいように改行や空白行を入れているが、実際には改行コードは含まれず、全ての行が繋がっている。
<p><strong>WordPressの抜粋で改行が削除される原因は wp_posts テーブルの post_content</strong><br />
<a rel="tag" class="hashtag u-tag u-category" href="https://ishii00141.stars.ne.jp/tag/gemini/">#Gemini</a>
<a rel="tag" class="hashtag u-tag u-category" href="https://ishii00141.stars.ne.jp/tag/php/">#PHP</a>
<a rel="tag" class="hashtag u-tag u-category" href="https://ishii00141.stars.ne.jp/tag/wordpress/">#WordPress</a>
<a rel="tag" class="hashtag u-tag u-category" href="https://ishii00141.stars.ne.jp/tag/%e4%bb%95%e6%a7%98/">#仕様</a></p>
<p> WordPressの入力時にEnterキーを叩くと段落が変わるのだが、単に改行したいこともあり、その場合はShiftキーを押しながらEnterキーを叩く。<br />
そうすると、段落が変わらずに改行することができる。<br />
そもそも、「段落」という表現が日本語と異なっているから困る。改行して一文字字下げすれば、それは新しい段落なのに、htmlの世界ではそうはみなさない。</p>
<p> それはともかく、WordPressの抜粋プログラムではhtmlタグを削除するのだが、次のようなコードの場合、Web表示では改行があるのに、タグを削除しちゃうと改行が無くなる。</p>
<p>見つかりません<br />Account h [...続きは下のURLで]</p>
<p><a href="https://ishii00141.stars.ne.jp/20251203-1656-4312/">https://ishii00141.stars.ne.jp/20251203-1656-4312/</a></p>
これを見た時に<br />に気づいた。
この content を作成しているのは、activitypub/includes/transformer/class-post.php の次の部分だろう。
/**
* Returns the content for the ActivityPub Item.
*
* The content will be generated based on the user settings.
*
* @return string The content.
*/
protected function get_content() {
// Remove Content from drafts.
if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->item ) ) {
return \__( '(This post is being modified)', 'activitypub' );
}
global $post;
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = $this->item;
$content = $this->get_post_content_template();
/**
* Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
*
* Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here.
*
* @param \WP_Post $post The post object.
*/
\do_action( 'activitypub_before_get_content', $post );
// It seems that shortcodes are only applied to published posts.
if ( is_preview() ) {
$post->post_status = 'publish';
}
// Register our shortcodes just in time.
Shortcodes::register();
// Fill in the shortcodes.
\setup_postdata( $post );
$content = \do_shortcode( $content );
\wp_reset_postdata();
$content = \wpautop( $content );
$content = \preg_replace( '/[\n\r\t]/', '', $content );
$content = \trim( $content );
// Don't need these anymore, should never appear in a post.
Shortcodes::unregister();
/**
* Filters the post content after it was transformed for ActivityPub.
*
* @param string $content The transformed post content.
* @param \WP_Post $post The post object being transformed.
*/
return \apply_filters( 'activitypub_the_content', $content, $post );
}
このwpautop()関数はプラグインではなく、WordPressが用意している関数で、wp-includes/formatting.php の中にあり、次のようなコードらしい。
/**
* Replaces double line breaks with paragraph elements.
*
* A group of regex replaces used to identify text formatted with newlines and
* replace double line breaks with HTML paragraph tags. The remaining line breaks
* after conversion become `<br />` tags, unless `$br` is set to '0' or 'false'.
*
* @since 0.71
*
* @param string $text The text which has to be formatted.
* @param bool $br Optional. If set, this will convert all remaining line breaks
* after paragraphing. Line breaks within `<script>`, `<style>`,
* and `<svg>` tags are not affected. Default true.
* @return string Text which has been converted into correct paragraph tags.
*/
function wpautop( $text, $br = true ) {
$pre_tags = array();
if ( '' === trim( $text ) ) {
return '';
}
// Just to make things a little easier, pad the end.
$text = $text . "\n";
/*
* Pre tags shouldn't be touched by autop.
* Replace pre tags with placeholders and bring them back after autop.
*/
if ( str_contains( $text, '<pre' ) ) {
$text_parts = explode( '</pre>', $text );
$last_part = array_pop( $text_parts );
$text = '';
$i = 0;
foreach ( $text_parts as $text_part ) {
$start = strpos( $text_part, '<pre' );
// Malformed HTML?
if ( false === $start ) {
$text .= $text_part;
continue;
}
$name = "<pre wp-pre-tag-$i></pre>";
$pre_tags[ $name ] = substr( $text_part, $start ) . '</pre>';
$text .= substr( $text_part, 0, $start ) . $name;
++$i;
}
$text .= $last_part;
}
// Change multiple <br>'s into two line breaks, which will turn into paragraphs.
$text = preg_replace( '|<br\s*/?>\s*<br\s*/?>|', "\n\n", $text );
$allblocks = '(?:table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|form|map|area|blockquote|address|style|p|h[1-6]|hr|fieldset|legend|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary)';
// Add a double line break above block-level opening tags.
$text = preg_replace( '!(<' . $allblocks . '[\s/>])!', "\n\n$1", $text );
// Add a double line break below block-level closing tags.
$text = preg_replace( '!(</' . $allblocks . '>)!', "$1\n\n", $text );
// Add a double line break after hr tags, which are self closing.
$text = preg_replace( '!(<hr\s*?/?>)!', "$1\n\n", $text );
// Standardize newline characters to "\n".
$text = str_replace( array( "\r\n", "\r" ), "\n", $text );
// Find newlines in all elements and add placeholders.
$text = wp_replace_in_html_tags( $text, array( "\n" => ' <!-- wpnl --> ' ) );
// Collapse line breaks before and after <option> elements so they don't get autop'd.
if ( str_contains( $text, '<option' ) ) {
$text = preg_replace( '|\s*<option|', '<option', $text );
$text = preg_replace( '|</option>\s*|', '</option>', $text );
}
/*
* Collapse line breaks inside <object> elements, before <param> and <embed> elements
* so they don't get autop'd.
*/
if ( str_contains( $text, '</object>' ) ) {
$text = preg_replace( '|(<object[^>]*>)\s*|', '$1', $text );
$text = preg_replace( '|\s*</object>|', '</object>', $text );
$text = preg_replace( '%\s*(</?(?:param|embed)[^>]*>)\s*%', '$1', $text );
}
/*
* Collapse line breaks inside <audio> and <video> elements,
* before and after <source> and <track> elements.
*/
if ( str_contains( $text, '<source' ) || str_contains( $text, '<track' ) ) {
$text = preg_replace( '%([<\[](?:audio|video)[^>\]]*[>\]])\s*%', '$1', $text );
$text = preg_replace( '%\s*([<\[]/(?:audio|video)[>\]])%', '$1', $text );
$text = preg_replace( '%\s*(<(?:source|track)[^>]*>)\s*%', '$1', $text );
}
// Collapse line breaks before and after <figcaption> elements.
if ( str_contains( $text, '<figcaption' ) ) {
$text = preg_replace( '|\s*(<figcaption[^>]*>)|', '$1', $text );
$text = preg_replace( '|</figcaption>\s*|', '</figcaption>', $text );
}
// Remove more than two contiguous line breaks.
$text = preg_replace( "/\n\n+/", "\n\n", $text );
// Split up the contents into an array of strings, separated by double line breaks.
$paragraphs = preg_split( '/\n\s*\n/', $text, -1, PREG_SPLIT_NO_EMPTY );
// Reset $text prior to rebuilding.
$text = '';
// Rebuild the content as a string, wrapping every bit with a <p>.
foreach ( $paragraphs as $paragraph ) {
$text .= '<p>' . trim( $paragraph, "\n" ) . "</p>\n";
}
// Under certain strange conditions it could create a P of entirely whitespace.
$text = preg_replace( '|<p>\s*</p>|', '', $text );
// Add a closing <p> inside <div>, <address>, or <form> tag if missing.
$text = preg_replace( '!<p>([^<]+)</(div|address|form)>!', '<p>$1</p></$2>', $text );
// If an opening or closing block element tag is wrapped in a <p>, unwrap it.
$text = preg_replace( '!<p>\s*(</?' . $allblocks . '[^>]*>)\s*</p>!', '$1', $text );
// In some cases <li> may get wrapped in <p>, fix them.
$text = preg_replace( '|<p>(<li.+?)</p>|', '$1', $text );
// If a <blockquote> is wrapped with a <p>, move it inside the <blockquote>.
$text = preg_replace( '|<p><blockquote([^>]*)>|i', '<blockquote$1><p>', $text );
$text = str_replace( '</blockquote></p>', '</p></blockquote>', $text );
// If an opening or closing block element tag is preceded by an opening <p> tag, remove it.
$text = preg_replace( '!<p>\s*(</?' . $allblocks . '[^>]*>)!', '$1', $text );
// If an opening or closing block element tag is followed by a closing <p> tag, remove it.
$text = preg_replace( '!(</?' . $allblocks . '[^>]*>)\s*</p>!', '$1', $text );
// Optionally insert line breaks.
if ( $br ) {
// Replace newlines that shouldn't be touched with a placeholder.
$text = preg_replace_callback( '/<(script|style|svg|math).*?<\/\\1>/s', '_autop_newline_preservation_helper', $text );
// Normalize <br>.
$text = str_replace( array( '<br>', '<br/>' ), '<br />', $text );
// Replace any new line characters that aren't preceded by a <br /> with a <br />.
$text = preg_replace( '|(?<!<br />)\s*\n|', "<br />\n", $text );
// Replace newline placeholders with newlines.
$text = str_replace( '<WPPreserveNewline />', "\n", $text );
}
// If a <br /> tag is after an opening or closing block tag, remove it.
$text = preg_replace( '!(</?' . $allblocks . '[^>]*>)\s*<br />!', '$1', $text );
// If a <br /> tag is before a subset of opening or closing block tags, remove it.
$text = preg_replace( '!<br />(\s*</?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)[^>]*>)!', '$1', $text );
$text = preg_replace( "|\n</p>$|", '</p>', $text );
// Replace placeholder <pre> tags with their original content.
if ( ! empty( $pre_tags ) ) {
$text = str_replace( array_keys( $pre_tags ), array_values( $pre_tags ), $text );
}
// Restore newlines in all elements.
if ( str_contains( $text, '<!-- wpnl -->' ) ) {
$text = str_replace( array( ' <!-- wpnl --> ', '<!-- wpnl -->' ), "\n", $text );
}
return $text;
}
/**
* Newline preservation help function for wpautop().
*
* @since 3.1.0
* @access private
*
* @param array $matches preg_replace_callback matches array
* @return string
*/
function _autop_newline_preservation_helper( $matches ) {
return str_replace( "\n", '<WPPreserveNewline />', $matches[0] );
}
この関数で、<br> ではなく <br /> に改行タグが統一されている。この関数を使っているので、Fediverse に送る content の改行タグが <br /> になったらしい。
最後に、Geminiに <br> ではなく <br /> が使われている理由を尋ねてみた。

現在のhtmlの標準的な<br>ではなく、今ではほとんど使われていない<br />に統一するコードになっているのはどうしてですか?
はい、それは非常に良い質問です。
標準の
wpautop()関数が<br />(スラッシュあり) に統一するようになっている主な理由は、WordPressが長年準拠してきた古いWeb標準と、その後の互換性を維持するためです。
🏛️
<br />に統一する理由主な理由は、以下の2点にあります。
1. XHTML との互換性の維持
- XHTML の要件: かつて Web 標準として広く採用されていた XHTML(Extensible HyperText Markup Language)では、HTMLをXMLの文法で書くことが求められました。XMLでは、すべての要素は必ず閉じる必要があり、子要素を持たないタグ(空要素、
<img>,<br>など)は、自己終了タグとして<br />のようにスラッシュを付けて閉じることが必須とされていました。- WordPress の歴史: WordPressは、XHTMLが主流だった時代から開発されており、
wpautop()はその時代に設計されました。そのため、生成するHTMLが XML/XHTML の文法に従うように、<br />を標準として採用しました。2. ブラウザ間の安定した動作
wpautop()が処理の途中で既存の<br>や<br/>を<br />に統一するのは、その後の正規表現による置換処理(例:(?<!<br />)の否定後読み)が、複数のタグ形式を気にせず、一貫した形式で動作するようにするためです。これにより、ブラウザ間の互換性や処理の信頼性が高まります。
🌐 HTML5の現在の標準
ご指摘の通り、HTML5 の現在の仕様では、自己終了タグ(空要素)について、スラッシュの有無は任意とされています。
- HTML5 で有効な形式:
<br>(スラッシュなし)と<br />(スラッシュあり)のどちらも正しく、機能的には同じです。- 推奨: HTML5では、冗長なスラッシュを省略した
<br>の方が、よりシンプルであるため推奨される傾向にあります。しかし、
wpautop()は古い資産であるため、後方互換性と安定性を優先し、現在も<br />を標準として出力し続けているのです。


コメント
<br>や<br />が表示されてない。
リモート返信
元のコメントの URL
あなたのプロフィール
このリプライは、Mastodonの方で「削除して下書きに戻す」を行った時に削除されたもの。
<br>や<br />が表示されてない。
じゃなくて、改行タグとして機能させちゃってる。
リモート返信
元のコメントの URL
あなたのプロフィール
この記事のap_outboxのpost_contentのcontentは次のようになってる。読めるようにutf-8の部分をデコードしてある。URLも半角スペースを挿入してURLと認識されないようにしてある。
またバグか…。
このバグは generate_post_summary()関数と、私のフック内の activitypub_get_full_content_for_trimming()関数にある html_entity_decode()関数にあり、html_entity_decode()関数によるデコードをコメントアウトすることで修正されるらしい。
この対処法だと、文字数のカウントに問題が生じるので、却下。