Site cover image

Site icon imagehtrkwn.dat

Just a htrkwn's personal hobby scrapbook.

💎小説投稿サイト等のルビ記法をJavaScriptを使ってRubyタグに変換する

ちまちま小説書いてるサイトでPHP製のMarkdownパーサーを使用しているがMarkdownにはルビ記法ないのなんでなんだ……という苦しみからクライアントサイドで実装(多分サーバーサイドでやるほうが良いと思う、多分)。

実装方法も考え方もほぼほぼ次の参考記事の通りです。感謝しかない。自分はほんのちょっと変更して、jQuery使わず他にpixiv記法を追加した。

参考:各種小説投稿サイトのルビ記法をJavaScriptで実現する - Qiita

💡
2023/07/26追記
マークダウンファイルから記事移すときに不要なバックスラッシュがコードサンプル中に入ってたのを削除しました

実装例

script.js

/**
 * ルビタグのテンプレート。
 * @constant
 * @type {string}
 */
const rubyTemplate = "<ruby>$1<rt>$2</rt></ruby>";

/**
 * ルビタグのための正規表現パターンとその置換テキストのリスト。
 * 各要素は以下のプロパティを持つオブジェクトです:
 *   - `pattern`: ルビの検出に使われる正規表現パターン。正規表現オブジェクトまたは文字列が必要です。
 *   - `replacement`: `pattern`にマッチした部分の置換テキスト。文字列が必要です。
 * 
 * @constant
 * @type {Array<{pattern: (RegExp|string), replacement: string}>}
 */
const rubyRegexList = [
	{ pattern: /[\||](.+?)《(.+?)》/g, replacement: rubyTemplate },
	{ pattern: /[\||](.+?)((.+?))/g, replacement: rubyTemplate },
	{ pattern: /[\||](.+?)\((.+?)\)/g, replacement: rubyTemplate },
	{ pattern: /\[\[rb:(.+?) &gt; (.+?)\]\]/g, replacement: rubyTemplate },
	{ pattern: /([\p{sc=Han}]+)《(.+?)》/gu, replacement: rubyTemplate },
	{ pattern: /([\p{sc=Han}]+)(([\p{sc=Hiragana}\p{sc=Katakana}ー~]+?))/gu, replacement: rubyTemplate },
	{ pattern: /([\p{sc=Han}]+)\(([\p{sc=Hiragana}\p{sc=Katakana}ー~]+?)\)/gu, replacement: rubyTemplate },
	{ pattern: /[\||]《(.+?)》/g, replacement: "《$1》" },
	{ pattern: /[\||]((.+?))/g, replacement: "($1)" },
	{ pattern: /[\||]\((.+?)\)/g, replacement: "($1)" }
];

/**
 * ルビタグのための正規表現マップのリスト。
 * @constant
 * @type {Array<Object>}
 */
const rubyRegexMapList = rubyRegexList.map(({ pattern, replacement }) => ({
	pattern: new RegExp(pattern),
	replacement
}));

/**
 * 与えられた文字列の中のルビタグを置換する。
 *
 * @param {string} str - ルビタグを置換する対象の文字列。
 * @returns {string} - ルビタグが置換された文字列。
 */
const replaceRubyTags = (str) => {
	return rubyRegexMapList.reduce((acc, { pattern, replacement }) => {
		return acc.replace(pattern, replacement);
	}, str);
}

/**
 * 記事を更新し、そのHTMLコンテンツのルビタグを置換する。
 *
 * @param {HTMLElement} el - 更新する記事のHTML要素。
 */
const updateArticle = (el) => {
	const replacedHtml = replaceRubyTags(el.innerHTML);
	el.innerHTML = replacedHtml;
}

/**
 * updateArticle関数をモジュールとしてエクスポートする。
 */
export default updateArticle;

index.html

<script type="module">
  import updateArticle from './script.js';

  const articleBody = document.getElementById("article-body");
  updateArticle(articleBody);
</script>

ESModuleとかでimportして使えるようにしたかったのでこんな感じでどうだろう。

正規表現について

正規表現のマッチングには Unicode プロパティエスケープ - JavaScript | MDN を使うようにした。

カタカナがややこしいことになってるんですが、まあ自分用なので多少妥協しました。

// https://util.unicode.org/UnicodeJsps/list-unicodeset.jsp
// :Script_Extensions=

[:Script_Extensions=Hira,Kana:]
[:Script_Extensions=Hani,Hira,Kana:]
[:Script_Extensions=Bopo,Hang,Hani,Hira,Kana:]
[:Script_Extensions=Bopo,Hang,Hani,Hira,Kana,Yiii:]

ユニコードプロパティについて

なんのこっちゃよくわからなかったのでChatGPT(GPT-4)に聞いてみたら次の回答を得ました。

この質問にある「[:Script_Extensions=Hira,Kana:]」などの表現は、ユニコードプロパティの一部を指定しているようです。ユニコードとは、全世界の文字を統一的に扱うための文字コード規格で、この中には非常に多くの文字とその特性を定義するプロパティが含まれています。

Script_Extensionsプロパティは、その文字がどのスクリプト(書記体系)に属するかを示すプロパティです。例えば、「[:Script_Extensions=Hira,Kana:]」は、平仮名(Hira)またはカタカナ(Kana)のスクリプトに属する文字を指定します。

以下はそれぞれのスクリプトの説明です。

  • Hira (Hiragana): 日本のひらがな
  • Kana (Katakana): 日本のカタカナ
  • Hani (Han): 漢字を含む、中国の文字体系
  • Bopo (Bopomofo): 中国語の注音符号
  • Hang (Hangul): 韓国のハングル
  • Yiii (Yi): 中国の少数民族である彝族の文字

例えば、「[:Script_Extensions=Hira,Kana:]」なら、この集合に含まれる文字はひらがなまたはカタカナ、「[:Script_Extensions=Bopo,Hang,Hani,Hira,Kana,Yiii:]」なら、これら全てのスクリプトに属する文字が含まれます。これは、例えば正規表現で特定のスクリプトの文字を検索したり、フィルタリングしたりするときに使われることがあります。

置換処理について

Array.prototype.reduce() - JavaScript | MDN

なかなか配列でのreduceメソッドを理解できてないんだけどこんなこともできるんだね……っていうのをジピって知りました。有難うOpenAI。

コード解説を chatGPT 3.5 にお願いしてみた

このコードは、RubyタグをHTMLに追加するための正規表現と置換ルールを定義し、記事本文の要素に適用するJavaScript関数を提供しています。

まず、rubyTemplateは、Rubyタグを含むHTMLテンプレート文字列を定義しています。$1$2は、後でマッチしたテキストの置換に使用されます。

const rubyTemplate = "<ruby>$1<rt>$2</rt></ruby>";

次に、rubyRegexListは、テキスト中でRubyタグを置換するために使用される正規表現と置換ルールのリストを定義します。各エントリは、patternreplacementフィールドを持ちます。patternは正規表現、replacementは置換するHTMLテンプレートです。

const rubyRegexList = [
	{ pattern: /[\||](.+?)《(.+?)》/g, replacement: rubyTemplate },
	{ pattern: /[\||](.+?)((.+?))/g, replacement: rubyTemplate },
	{ pattern: /[\||](.+?)\((.+?)\)/g, replacement: rubyTemplate },
	{ pattern: /\[\[rb:(.+?) &gt; (.+?)\]\]/g, replacement: rubyTemplate },
	{ pattern: /([\p{sc=Han}]+)《(.+?)》/gu, replacement: rubyTemplate },
	{ pattern: /([\p{sc=Han}]+)(([\p{sc=Hiragana}\p{sc=Katakana}ー~]+?))/gu, replacement: rubyTemplate },
	{ pattern: /([\p{sc=Han}]+)\(([\p{sc=Hiragana}\p{sc=Katakana}ー~]+?)\)/gu, replacement: rubyTemplate },
	{ pattern: /[\||]《(.+?)》/g, replacement: "《$1》" },
	{ pattern: /[\||]((.+?))/g, replacement: "($1)" },
	{ pattern: /[\||]\((.+?)\)/g, replacement: "($1)" }
];

このリストは、以下のようなテキストのパターンに一致するものを含みます。

  • |text《ruby》|text《ruby》|text(ruby)|text(ruby)|text(ruby)|text(ruby)に一致するもの
  • [[rb:ruby &gt; text]]に一致するもの
  • 漢字《ruby》漢字(kana)漢字(kana)に一致するもの
  • |《ruby》|《ruby》|(ruby)|(ruby)|(ruby)|(ruby)に一致するもの

次に、rubyRegexMapListは、rubyRegexListの各エントリを正規表現オブジェクトに変換するための配列です。map関数を使用して、オブジェクトを変換し、new RegExp()を使用して、各パターンを正規表現オブジェクトに変換しています。

const rubyRegexMapList = rubyRegexList.map(({ pattern, replacement }) => ({
  pattern: new RegExp(pattern),
  replacement
}));

replaceRubyTags関数は、記事の本文の要素を受け取り、各Rubyタグを置換するために、rubyRegexMapListの各正規表現をループで処理し、replace()メソッドを使用して、テキストの置換を実行します。

const replaceRubyTags = (str) => {
  return rubyRegexMapList.reduce((acc, { pattern, replacement }) => {
    return acc.replace(pattern, replacement);
  }, str);
};

最後に、updateArticle関数は、記事の本文の要素を受け取り、replaceRubyTags関数を使用して、その要素内のテキストを置換し、結果を要素のinnerHTMLプロパティに設定します。

const updateArticle = (el) => {
  const replacedHtml = replaceRubyTags(el.innerHTML);
  el.innerHTML = replacedHtml;
};

このコードは、Rubyタグを含むテキストをHTMLに変換するために必要な正規表現と置換ルールを提供し、それを実際に記事本文の要素に適用するJavaScript関数を提供しています。