自動整形のどこに問題点があるか

例えば、DWなどで作成したHTMLソースをHTMLエディタにそのまま貼り付けて記事を作成することがある(僕はないけど)。その場合、一度ビジュアルエディタに切り替えてまたすぐHTMLエディタに戻すと<p>と<br />が消去されている。つってもthe_content()で返っくるソースにはちゃんと<p>も<br />も挿入されてんだし、むしろHTMLエディタで編集中の場合にもっともよく使う<p>と<br />を入力しなくても済むんだから楽だなくらいに思ってました。
ところがちゃんと検証してみたら大きな欠陥があったんです。

まず、HTMLエディタに以下のソースを入れたとします。

<p>それには及びません。<br />
私は罪な男です。</p>

そしてビジュアルエディタに切り替えると、

それには及びません。

私は罪な男です。

「ん??」
これはおかしい。HTMLエディタに戻してみると、

<p>それには及びません。</p>
<p>私は罪な男です。</p>

なぜか2行とも<p>で括られてしまっている。
これは直さねばっつーわけで、その方法を記録しておく。
それと今回は<p>と<br />の除去も止めさせます。やっぱり始めてWordPressを使う人を混乱させるだけなんで。

editor.jsをハックする

wp-admin/js/editor.jsがエディタ切り替え時の自動整形を行なっているんで、これを改良するわけですが、コアファイルを直接弄るわけにはいかないので、このeditor.jsを停止させ、改良したコピーを読み込んでもらいます。コピーしたeditor.jsはテーマディレクトリに置きました。

wp_deregister_script( 'editor' );
wp_enqueue_script( 'editor', get_bloginfo( 'template_directory' ) . '/editor.js' );

これをfunctions.phpに書きます。
※たぶん他のハンドルも同じ要領でいけます。
http://codex.wordpress.org/Function_Reference/wp_enqueue_script#Default_scripts_included_with_WordPress

以下、改良後のソースです。(わかりやすく改行しています)

var switchEditors = {
	switchto:function(b) {
		var c = b.id, a = c.length, e = c.substr(0, a - 5), d = c.substr(a - 4);
		this.go(e, d);
	},
	go:function(g, f) {
		g = g || "content";
		f = f || "toggle";
		var c = this, b = tinyMCE.get(g), a, d, e = tinymce.DOM;
		a = "wp-" + g + "-wrap";
		d = e.get(g);
		if ("toggle" == f) {
			if (b&&!b.isHidden()) {
				f = "html";
			} else {
				f = "tmce";
			}
		}
		if ("tmce" == f || "tinymce" == f) {
			if (b&&!b.isHidden()) {
				return false;
			}
			if (typeof(QTags) != "undefined") {
				QTags.closeAllTags(g);
			}
			if (tinyMCEPreInit.mceInit[g] && tinyMCEPreInit.mceInit[g].wpautop) {
				d.value = c.wpautop(d.value);
			}
			if (b) {
				b.show();
			} else {
				b = new tinymce.Editor(g, tinyMCEPreInit.mceInit[g]);
				b.render();
			}
			e.removeClass(a, "html-active");
			e.addClass(a, "tmce-active");
			setUserSetting("editor", "tinymce");
		} else {
			if ("html" == f) {
				if (b&&b.isHidden()) {
					return false;
				}
				if (b) {
					d.style.height = b.getContentAreaContainer().offsetHeight + 20 + "px";
					b.hide();
				}
				e.removeClass(a, "tmce-active");
				e.addClass(a, "html-active");
				setUserSetting("editor", "html");
			}
		}
		return false;
	},
	_wp_Nop:function(b) {
		var c, a;

		if (b.indexOf("<pre") != -1 || b.indexOf("<script") != -1) {
			b = b.replace(/<(pre|script)[^>]*>[\s\S]+?<\/\1>/g, function(d) {
				d = d.replace(/<br ?\/?>(\r\n|\n)?/g, "<wp_temp>");
				return d.replace(/<\/?p( [^>]*)?>(\r\n|\n)?/g, "<wp_temp>");
			});
		}

		c = "blockquote|ul|ol|li|table|thead|tbody|tfoot|tr|th|td|div|h[1-6]|p|fieldset";

		b = b.replace(new RegExp("\\s*</("+c+")>\\s*", "g"), "</$1>\n");
		b = b.replace(new RegExp("\\s*<((?:"+c+")(?: [^>]*)?)>", "g"), "\n<$1>");
		b = b.replace(/(<p [^>]+>.*?)<\/p>/g, "$1</p#>");

		/*<p>は消したくないのでコメントアウト*/
		//b = b.replace(/<div( [^>]*)?>\s*<p>/gi, "<div$1>\n\n");
		//b = b.replace(/\s*<p>/gi, "");
		//b = b.replace(/\s*<\/p>\s*/gi, "\n\n");

		b = b.replace(/\n[\s\u00a0]+\n/g, "\n\n");

		/*<br />も消したくないのでコメントアウト*/
		//b = b.replace(/\s*<br ?\/?>\s*/gi, "\n");

			/*<br />の後に改行を入れる*/
			b = b.replace(/\s*<br ?\/?>\s*/gi, "<br />\n");

			/*<iframe>を括る<p>と<br />は除去*/
			b = b.replace(/<p>\s*<iframe( [^>]*)?>/gi, "<iframe$1>");
			b = b.replace(/<\/iframe>\s*(<\/p>|<br \/>)/gi, "</iframe>");

		b = b.replace(/\s*<div/g, "\n<div");
		b = b.replace(/<\/div>\s*/g, "</div>\n");
		/* この行は省略します。 */
		/* この行は省略します。 */
		a = "blockquote|ul|ol|li|table|thead|tbody|tfoot|tr|th|td|h[1-6]|pre|fieldset";
		b = b.replace(new RegExp("\\s*<((?:"+a+")(?: [^>]*)?)\\s*>", "g"), "\n<$1>");
		b = b.replace(new RegExp("\\s*</("+a+")>\\s*", "g"), "</$1>\n");
		b = b.replace(/<li([^>]*)>/g, "\t<li$1>");

		if (b.indexOf("<hr") != -1) {
			b = b.replace(/\s*<hr( [^>]*)?>\s*/g, "\n\n<hr$1>\n\n");
		}

		if (b.indexOf("<object") != -1) {
			b = b.replace(/<object[\s\S]+?<\/object>/g, function(d) {
				return d.replace(/[\r\n]+/g, "");
			});
		}

		b = b.replace(/<\/p#>/g, "</p>\n");
		b = b.replace(/\s*(<p [^>]+>[\s\S]*?<\/p>)/g, "\n$1");
		b = b.replace(/^\s+/, "");
		b = b.replace(/[\s\u00a0]+$/, "");
		b = b.replace(/<wp_temp>/g, "\n");

		return b;
	},
	_wp_Autop:function(a) {
		var b = "table|thead|tfoot|tbody|tr|td|th|caption|col|colgroup|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|address|math|p|h[1-6]|fieldset|legend|hr|noscript|menu|samp|header|footer|article|section|hgroup|nav|aside|details|summary";

		if (a.indexOf("<object") != -1) {
			a = a.replace(/<object[\s\S]+?<\/object>/g, function(c){return c.replace(/[\r\n]+/g, "")});
		}

		a = a.replace(/<[^<>]+>/g, function(c){return c.replace(/[\r\n]+/g, " ")});

		if (a.indexOf("<pre") != -1||a.indexOf("<script") != -1) {
			a = a.replace(/<(pre|script)[^>]*>[\s\S]+?<\/\1>/g, function(c) {
				return c.replace(/(\r\n|\n)/g, "<wp_temp_br>");
			});
		}

		a = a + "\n\n";
		a = a.replace(/<br \/>\s*<br \/>/gi, "\n\n");
		a = a.replace(new RegExp("(<(?:"+b+")(?: [^>]*)?>)", "gi"), "\n$1");
		a = a.replace(new RegExp("(</(?:"+b+")>)", "gi"), "$1\n\n");
		a = a.replace(/<hr( [^>]*)?>/gi, "<hr$1>\n\n");
		a = a.replace(/\r\n|\r/g, "\n");
		a = a.replace(/\n\s*\n+/g, "\n\n");
		a = a.replace(/([\s\S]+?)\n\n/g, "<p>$1</p>\n");
		a = a.replace(/<p>\s*?<\/p>/gi, "");
		a = a.replace(new RegExp("<p>\\s*(</?(?:"+b+")(?: [^>]*)?>)\\s*</p>", "gi"), "$1");
		a = a.replace(/<p>(<li.+?)<\/p>/gi, "$1");
		a = a.replace(/<p>\s*<blockquote([^>]*)>/gi, "<blockquote$1><p>");
		a = a.replace(/<\/blockquote>\s*<\/p>/gi, "</p></blockquote>");
		a = a.replace(new RegExp("<p>\\s*(</?(?:"+b+")(?: [^>]*)?>)", "gi"), "$1");
		a = a.replace(new RegExp("(</?(?:"+b+")(?: [^>]*)?>)\\s*</p>", "gi"), "$1");
		a = a.replace(/\s*\n/gi, "<br />\n");

			/*↑で余計についた<br />を削除*/
			a = a.replace(/<br ?\/?><br ?\/?>\n/gi, "<br />\n");

		a = a.replace(new RegExp("(</?(?:"+b+")[^>]*>)\\s*<br />", "gi"), "$1");
		a = a.replace(/<br \/>(\s*<\/?(?:p|li|div|dl|dd|dt|th|pre|td|ul|ol)>)/gi, "$1");
		/* この行は省略します。 */
		a = a.replace(/(<(?:div|th|td|form|fieldset|dd)[^>]*>)(.*?)<\/p>/g, function(e, d,f){if (f.match(/<p( [^>]*)?>/)){return e}return d+"<p>"+f+"</p>"});
		a = a.replace(/<wp_temp_br>/g, "\n");

		return a;
	},
	pre_wpautop:function(b) {
		var a = this, d = {o:a, data:b, unfiltered:b}, c = typeof(jQuery) != "undefined";
		if (c) {
			jQuery("body").trigger("beforePreWpautop", [d]);
		}
		d.data = a._wp_Nop(d.data);
		if (c) {
			jQuery("body").trigger("afterPreWpautop", [d]);
		}
		return d.data
	},
	wpautop:function(b) {
		var a = this, d = {o:a, data:b, unfiltered:b}, c = typeof(jQuery) != "undefined";
		if (c) {
			jQuery("body").trigger("beforeWpautop", [d]);
		}
		d.data = a._wp_Autop(d.data);
		if (c) {
			jQuery("body").trigger("afterWpautop", [d]);
		}
		return d.data;
	}
};

_wp_Autopの方はHTML→ビジュアル切り替え時に。
_wp_Nopの方はビジュアル→HTML及び、保存時のスクリプトです。

まず、HTMLに切り替えた時に消されてしまう<p>と<br />を保護します。以下4文をコメントアウトします。(71~73、78行目)

//b = b.replace(/<div( [^>]*)?>\s*<p>/gi, "<div$1>\n\n");
//b = b.replace(/\s*<p>/gi, "");
//b = b.replace(/\s*<\/p>\s*/gi, "\n\n");
//b = b.replace(/\s*<br ?\/?>\s*/gi, "\n");

コードを見やすくするために<br />の後に改行が欲しいので以下を追記します。(81行目)

b = b.replace(/\s*<br ?\/?>\s*/gi, "<br />\n");

先のコードで<p>と<br />を救ってしまったがために、<iframe></iframe>を括った<p>や<br />が残ってしまうので、これを除去します。

b = b.replace(/<p>\s*<iframe( [^>]*)?>/gi, "<iframe$1>");
b = b.replace(/<\/iframe>\s*(<\/p>|<br \/>)/gi, "</iframe>");

次に、ビジュアルエディタに切り替えた時にすべての改行コードが<br />に置き換えられてしまうところをカバーしてあげます。(147行目)

a = a.replace(/<br ?\/?><br ?\/?>\n/gi, "<br />\n");

以上です。

この行は省略します」が3箇所ありますが、これらにはcaptionタグに関するコードが書かれています。エディタが見事に置き換えてしまうので省略しています。
故に、コードをそのままコピペしても動作しません。
そしてこれはあくまで僕の場合なので参考程度にしてください。
※バージョン3.4.1で確認しています。

2012/08/05 加筆。