JavaScriptで簡単!自動ルビ生成ツールの作り方

当ページのリンクには広告が含まれています。

漢字にルビを振りたいとき、手動でつけていくのは大変ですよね。

最初は手動でつけていたんですが、ものすごく時間もかかるし首と肩と手が痛いので、どうにか自動化できないか考えてみました。

ChatGPTやGoogle Geminiを使えば、ある程度生成してくれるのですが、毎回同じように生成してほしくても上手くいきませんでした。

なので、自分で納得のいくツールを作れば、それを毎回使うことができますよね。

今回は「自動でルビを振ったHTML生成ツールの作り方」を紹介します。

以下が僕の作成した自動ルビ振りツールです。

文章ではなくHTMLを生成するので、後から誤字を簡単に編集できます。

できるだけ誤字がないようなツールを目指しましたが、多少の誤字は仕方がないですね。

WordPressとConoHa WINGを使います。

WordPressのテーマはSWELLを使っています。

大まかなやり方は他のサーバーやテーマでも同じだと思いますが、あらかじめご了承ください。

目次

1. HTMLを準備

まずは、HTMLを準備します。

僕はWordpressのカスタムHTMLを使いました。

WordPressで新規投稿を追加し、カスタムHTMLを準備します。

記事編集画面で「+」ボタンをクリックし、その中から「カスタムHTML」を選択します。

カスタムHTMLが挿入出来たら、以下のコードを貼り付けます。

<textarea id="inputText"></textarea>
<button id="convertBtn">ルビ変換</button>
<div id="outputText"></div>

このようになればOKです。

2. functions.phpを編集

続いて、functions.phpを編集していきます。

WordPressのダッシュボードから「外観」→「テーマファイルエディター」を選択します。

編集するテーマを「SWELL CHILD」にし、「テーマのための関数 (functions.php)」を選択します。

必ず子テーマを選択してください。

編集するのはスタイルシートではありません。

僕はずっとスタイルシートを編集していて、エラー続きで数時間無駄にしました笑

編集画面が表示出来たら、以下のコードを貼り付けます。

function enqueue_ruby_tool_script() {
    // kuromoji.js を子テーマの js フォルダから読み込む
    wp_enqueue_script('kuromoji', get_stylesheet_directory_uri() . '/js/kuromoji.js', array(), '0.1.2', true);

    // wanakana.js は CDN から読み込む(ルビや仮名変換用)
    wp_enqueue_script(
        'wanakana',
        'https://cdn.jsdelivr.net/npm/wanakana@4.0.2/umd/wanakana.min.js',
        array(),
        '4.0.2',
        true
    );

    // ruby-tool.js(依存関係 kuromoji と wanakana)
    wp_enqueue_script(
        'ruby-tool',
        get_stylesheet_directory_uri() . '/js/ruby-tool.js',
        array('kuromoji','wanakana'),
        '1.0',
        true
    );
}
add_action('wp_enqueue_scripts', 'enqueue_ruby_tool_script');

貼り付けたら「ファイルを更新」をクリックします。

致命的なエラーをチェックするためにサイトと通信できないため、PHP の変更は取り消されました。SFTP を使うなど、他の手段で PHP ファイルの変更をアップロードする必要があります。

上のようなエラーメッセージが表示される場合

上記のようなエラーメッセージが表示される場合、プラグインが関係している可能性があります。

僕の場合は「SiteGuard」というプラグインが関係していました。

この場合、アクセス制限を解除する必要があります。

ダッシュボードから「SiteGuard」→「管理ページアクセス制限」と進みます。

制限を「無効」にし、「変更を保存」をクリックします。

僕はこれで解決しました。

functions.phpを編集した後は、有効に戻すのを忘れないようにしましょう。

3. JavaScriptコードを準備

続いて、JavaScriptコード(JSコード)を準備していきます。

まずはパソコンでメモアプリを準備しましょう。

メモアプリに以下のコードを貼り付けます。

// ruby-tool.js
console.log("ruby-tool.js loaded");

document.addEventListener("DOMContentLoaded", () => {
  const input = document.getElementById("inputText");
  const output = document.getElementById("outputText");
  const convertBtn = document.getElementById("convertBtn");

  // テキストエリアを大きめ
  input.style.width = "100%";
  input.style.height = "200px";
  input.style.fontSize = "1rem";
  input.style.padding = "0.5rem";

  // ボタンスタイル
  [convertBtn].forEach(btn => {
    btn.style.backgroundColor = "#8224e3";
    btn.style.color = "white";
    btn.style.border = "none";
    btn.style.borderRadius = "0.5rem";
    btn.style.padding = "0.5rem 1rem";
    btn.style.fontSize = "1rem";
    btn.style.cursor = "pointer";
    btn.style.transition = "all 0.2s";
  });
  convertBtn.addEventListener("mouseover", () => {
    convertBtn.style.backgroundColor="#5c1bb3";
    convertBtn.style.transform = "scale(1.05)";
  });
  convertBtn.addEventListener("mouseout", () => {
    convertBtn.style.backgroundColor="#8224e3";
    convertBtn.style.transform = "scale(1)";
  });

  const numberReadings = { "0":"ぜろ","1":"いち","2":"に","3":"さん","4":"よん","5":"ご","6":"ろく","7":"なな","8":"はち","9":"きゅう" };
  const specialTimeReadings = {
    "1時":"いちじ","2時":"にじ","3時":"さんじ","4時":"よじ","5時":"ごじ",
    "6時":"ろくじ","7時":"しちじ","8時":"はちじ","9時":"くじ","10時":"じゅうじ",
    "11時":"じゅういちじ","12時":"じゅうにじ",
    "13時":"じゅうさんじ","14時":"じゅうよじ","15時":"じゅうごじ","16時":"じゅうろくじ",
    "17時":"じゅうしちじ","18時":"じゅうはちじ","19時":"じゅうくじ","20時":"にじゅうじ",
    "21時":"にじゅういちじ","22時":"にじゅうにじ","23時":"にじゅうさんじ","24時":"にじゅうよじ",
    "19時半":"じゅうくじはん",
    "22時半":"にじゅうにじはん"
  };
  const customReadings = {
    "一日":"いちにち","今日":"きょう","一人":"ひとり","二人":"ふたり",
    "後":"あと","淹":"い","買":"か","物":"もの","歩":"ある","疲":"つか","午後":"ごご"
  };

  function isKanji(char) { return /[\u4E00-\u9FFF]/.test(char); }
  function isNumber(char) { return /\d/.test(char); }

  function extractReading(surface, reading, kanjiBlock) {
    // customReadings があれば優先
    if (customReadings[kanjiBlock]) return customReadings[kanjiBlock];
    let readingKat = reading ? wanakana.toKatakana(reading) : "";
    const startIndex = surface.indexOf(kanjiBlock);
    const endIndex = startIndex + kanjiBlock.length;
    const prefix = surface.slice(0, startIndex);
    const suffix = surface.slice(endIndex);
    const prefixKat = prefix ? wanakana.toKatakana(prefix) : "";
    const suffixKat = suffix ? wanakana.toKatakana(suffix) : "";
    if (prefixKat && readingKat.startsWith(prefixKat)) readingKat = readingKat.slice(prefixKat.length);
    if (suffixKat && readingKat.endsWith(suffixKat)) readingKat = readingKat.slice(0, readingKat.length - suffixKat.length);
    return wanakana.toHiragana(readingKat);
  }

  function addRuby(surface, reading) {
    return surface.replace(/([\u4E00-\u9FFF]+)/g, (kanjiBlock) => {
      // customReadings対象は優先
      if (customReadings[kanjiBlock]) return `<ruby>${kanjiBlock}<rt>${customReadings[kanjiBlock]}</rt></ruby>`;
      const blockReading = extractReading(surface, reading, kanjiBlock);
      return `<ruby>${kanjiBlock}<rt>${blockReading}</rt></ruby>`;
    });
  }

  function generateRubyHTML(text, tokenizer) {
    const paragraphs = text.split(/\n+/).filter(p => p.trim() !== "");
    let html = '<div style="text-align:center;">\n';

    paragraphs.forEach(p => {
      const tokens = tokenizer.tokenize(p);
      let rubyLine = "";

      for (let i = 0; i < tokens.length; i++) {
        let t = tokens[i];
        let surface = t.surface_form;

        // 特殊時間対応
        if (i < tokens.length - 1) {
          let next = tokens[i+1].surface_form;
          let timeToken = surface + next;
          if (specialTimeReadings[timeToken]) {
            rubyLine += `<ruby>${timeToken}<rt>${specialTimeReadings[timeToken]}</rt></ruby>`;
            i++;
            continue;
          }
        }

        // customReadings優先
        if (customReadings[surface]) {
          rubyLine += `<ruby>${surface}<rt>${customReadings[surface]}</rt></ruby>`;
        }
        else if (surface.split("").some(isKanji)) {
          rubyLine += addRuby(surface, t.reading || "");
        }
        else if (surface.split("").some(isNumber)) {
          const hira = surface.split("").map(d => numberReadings[d] || d).join("");
          rubyLine += `<ruby>${surface}<rt>${hira}</rt></ruby>`;
        }
        else {
          rubyLine += surface;
        }
      }

      html += `<p>${rubyLine}</p>\n`;
    });

    html += '</div>';
    return html;
  }

  // コピーボタン
  const copyBtn = document.createElement("button");
  copyBtn.textContent = "コピー";
  copyBtn.style.backgroundColor = "#8224e3";
  copyBtn.style.color = "white";
  copyBtn.style.border = "none";
  copyBtn.style.borderRadius = "0.5rem";
  copyBtn.style.padding = "0.5rem 1rem";
  copyBtn.style.fontSize = "1rem";
  copyBtn.style.cursor = "pointer";
  copyBtn.style.marginLeft = "5px";
  copyBtn.style.transition = "all 0.2s";
  copyBtn.addEventListener("mouseover", () => {
    copyBtn.style.backgroundColor="#5c1bb3";
    copyBtn.style.transform = "scale(1.05)";
  });
  copyBtn.addEventListener("mouseout", () => {
    copyBtn.style.backgroundColor="#8224e3";
    copyBtn.style.transform = "scale(1)";
  });

  convertBtn.insertAdjacentElement("afterend", copyBtn);

  convertBtn.addEventListener("click", () => {
    if (!window.kuromoji) { alert("Tokenizer not ready"); return; }
    kuromoji.builder({ dicPath: '/wp-content/themes/swell_child/js/dict/' }).build((err, tokenizer) => {
      if (err) { console.error(err); return; }

      const htmlCode = generateRubyHTML(input.value, tokenizer);

      // 出力ボックスにだけ背景色(薄グレー)
      output.innerHTML = `<div style="padding:10px; background-color:#f5f5f5;"><pre><code>${htmlCode.replace(/</g,"&lt;").replace(/>/g,"&gt;")}</code></pre></div>`;

      copyBtn.onclick = () => {
        navigator.clipboard.writeText(htmlCode).then(() => {
          copyBtn.textContent = "コピーしました";
          setTimeout(() => copyBtn.textContent = "コピー", 1500);
        });
      };
    });
  });
});

コードを貼り付けたら、このテキストファイルを以下の名前で保存します。

ruby-tool.js

4. kuromojiファイルを準備

続いて、kuromojiファイルを準備していきます。

Kuromojiファイルがないと、自動で正しい読み仮名を取得できないので、自動ルビ生成には必須です。

以下のリンクからダウンロードページに移動できます。

kuromojiファイルダウンロードページ

ダウンロードページに移動したら、緑色の「Code」ボタンをクリックし、「Download ZIP」をクリックします。

Zipファイルがダウンロード出来たら、右クリックから「すべて展開」を選択してファイルを解凍しましょう。

現時点では「kuromoji.js-master」というファイル名になります。

5. サーバーにファイルをアップロード

続いて、準備したJSコードとkuromojiファイルをサーバーにアップロードしていきます。

レンタルサーバーのConoHa WINGにログインしましょう。

「サイト管理」→「ファイルマネージャー」を選択します。

編集したいサイトであることを確認してください。

もし違う場合は「切り替え」ボタンから選択してください。

ファイルマネージャーが開けたら「public_html」をクリックします。

自分のサイトフォルダの中にある「wp-content」をクリックします。

wp-contentの中から「themes」を探し、さらにその中にある「swell_child」をクリックします。

swell_childフォルダの中に「js」という名前で新規フォルダを作成します。

右クリックをして「新規フォルダ」から作成できます。

jsフォルダが作成出来たら、フォルダをダブルクリックして開きます。

この中にJSコードとkuromojiファイルをアップロードしていきます。

kuromoji.js-masterの中にたくさんのフォルダがあると思います。

この中の「dict」というフォルダをまずはアップロードしましょう。(フォルダ丸ごと)

ConoHa WINGのファイルマネージャーにドラッグ&ドロップすることでアップロードできます。

続いて、「build」というフォルダの中にある「kuromoji.js」というファイルをアップロードします。

「kuromoji.js」は単体でアップロードしてください。

続いて、最初に準備した「ruby-tool.js」ファイルをアップロードします。

swell_childの中のjsフォルダの中に、上画像のようにアップロードされていればOKです。

これで準備完了です。

ファイルマネージャーにアップロードした後は、パソコンのエクスプローラーにあるファイルは削除しても構いません。

【補足】ConoHa WINGのWAF設定

ConoHa WINGのWAF設定がオンになっていると、Kuromoji の辞書ファイル(.dat.gz)が「怪しいファイル」と誤認され、上手くツールが機能しません。

ただ、WAF設定を常にオフにしておくのはセキュリティ上安全とは言えませんよね。

そこで、WAF設定をオンにしたまま、このツールを使う方法を紹介します。

まずは、WAF設定をオンにして、一度ツールを試してみてください。(ログを表示させるため)

ConoHa WINGの管理画面から「サイト設定」→「サイトセキュリティ」と進みます。

表示切替を「ログ」にし、画面真ん中の「…」をクリックします。

ここに表示されるURLが以下の場合、「除外」ボタンをクリックします。

http://あなたのドメイン/wp-content/themes/swell_child/js/dict/check.dat.gz

これで、ConoHa WINGのWAF設定をオンにしたまま、自動ルビ生成ツールを使用することができます。

まとめ

いかがだったでしょうか。

プログラミング知識が0の僕は、このツールを作るために丸一日かかりました。

このツールは完璧ではなく、「一人」は「いちにん」、「二人」は「ににん」、「12時」は「いちにじ」などと読んでしまいます。

何度も修正したのですが、今度は他の漢字が崩れたりするので、少しの誤字は手動で修正することにしました。

このツールがあれば十分なんですが、もしかしたら自分も作ってみたいという方がいるかもしれないと思ったので、この記事を作成しました。

少しでも参考になったら嬉しいです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

2001年生まれ。
「自分の幸せ」を追求しています。
日々の気づきや思ったこと、シェアしたいこと、とにかく書きたいことを書く雑記ブログです。

ワンピース、進撃の巨人、トリコ好き。

目次