目次
ブックマーク
応援する
いいね!
コメント
シェア
通報

ふりがな付きテキストエディターVer.3.1

ネオページ作家のAlgoLighter様よりアドバイスをいただき、傍点の機能追加やふりがな機能の詳細化が実現しました。ありがとうございます。


------------------------------------------------


## ふりがな付きテキストエディター 全機能一覧


### テキスト処理機能

**ふりがな記法対応**

- |漢字《       》 - 基本のふりがな記法

- 漢字(          ) - 括弧を使ったふりがな記法

- |漢字《        》 - 青空文庫形式(範囲明示)

- |漢字(かな) - 全角括弧に変換(ルビ化しない)

- |(かな) - 全角括弧に変換のみ


**傍点(圏点)機能**

- - 傍点による強調表示

- 日本語小説で使われる黒丸の傍点


### 文字数カウント機能

**ほぼ正確な文字数計測**

(ネオページの文字数カウントと同じになるよう設定していますが、プラマイ2文字のズレが出ます)

- 漢字、ひらがな、カタカナ、記号を分類カウント

- ふりがな指定記号は文字数に含まない

- 空白・改行・タブを除外

- リアルタイム更新

- デバッグ情報をコンソールに出力


### レイアウト・表示機能

**左右分割エディター**

- 左側:テキスト入力エリア

- 右側:ふりがな付きプレビュー

- リサイザーで左右の幅を自由調整

- 美しい斜め縞模様のリサイザー(青⇔オレンジ)


**表示モード切り替え**

- 固定式:400px固定高でスクロール表示

- スクロール式:内容に合わせて高さ自動調整

- スクロール式では左右が連動して同じ高さまで伸びる


**ふりがな表示制御**

- ふりがなON/OFF切り替えスイッチ

- 校正時の確認に便利


### ユーザーインターフェース

**視覚的デザイン**

- グラデーション背景とカード式レイアウト

- ホバー効果とアニメーション

- 統計カードでの視覚的文字数表示

- レスポンシブデザイン(PC・モバイル対応)


**操作性向上**

- リアルタイムプレビュー更新

- 上に戻るボタン(常時表示)

- スムーズスクロール

- タッチ操作対応


### モバイル対応

**レスポンシブレイアウト**

- 縦積み表示に自動切り替え

- モバイル用リサイザー(横向き縞模様)

- タッチ操作でのリサイズ対応

- 画面サイズに応じたUI調整


------------------------------------------------

(ここからコピー)




<!DOCTYPE html>

<html lang="ja">

<head>

<meta charset="UTF-8">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>ふりがな付きテキストエディター</title>

<style>

* {

margin: 0;

padding: 0;

box-sizing: border-box;

}


body {

font-family: 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif;

background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

min-height: 100vh;

}


.header {

background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);

color: white;

padding: 30px;

text-align: center;

}


.header h1 {

font-size: 2.5em;

margin-bottom: 10px;

font-weight: 300;

}


.header p {

opacity: 0.9;

font-size: 1.1em;

}


.container {

display: flex;

min-height: 600px;

margin: 20px;

background: white;

border-radius: 20px;

box-shadow: 0 20px 40px rgba(0,0,0,0.1);

overflow: hidden;

}


.panel {

flex: 1 1 0;

padding: 30px;

overflow: auto;

background: #fff;

min-width: 180px;

position: relative;

}


.input-panel {

background: #f8f9fa;

}


.resizer {

width: 20px;

background: repeating-linear-gradient(

135deg,

#667eea 0 5px,

#fff 5px 10px

);

cursor: col-resize;

display: flex;

align-items: center;

justify-content: center;

position: relative;

z-index: 10;

transition: background 0.3s;

user-select: none;

}


.resizer:hover {

background: repeating-linear-gradient(

135deg,

#ff6600 0 5px,

#fff 5px 10px

);

}


.resizer-grip {

width: 8px;

height: 48px;

background: #333;

border-radius: 4px;

box-shadow: 0 0 0 2px #fff, 0 0 6px rgba(102, 126, 234, 0.5);

opacity: 0.8;

}


.resizer:hover .resizer-grip {

box-shadow: 0 0 0 2px #fff, 0 0 6px rgba(255, 102, 0, 0.5);

}


.section-title {

font-size: 1.3em;

margin-bottom: 20px;

color: #2c3e50;

font-weight: 500;

display: flex;

align-items: center;

gap: 10px;

}


.section-title::before {

content: '';

width: 4px;

height: 20px;

background: linear-gradient(135deg, #667eea, #764ba2);

border-radius: 2px;

}


.instructions {

background: #e8f4fd;

border: 1px solid #bee5eb;

border-radius: 10px;

padding: 20px;

margin-bottom: 20px;

}


.instructions h3 {

color: #0c5460;

margin-bottom: 10px;

font-size: 1.1em;

}


.instructions p {

color: #0c5460;

line-height: 1.6;

margin-bottom: 8px;

}


.instructions code {

background: rgba(12, 84, 96, 0.1);

padding: 2px 6px;

border-radius: 4px;

font-family: 'Courier New', monospace;

}


.input-textarea {

width: 100%;

height: 400px;

padding: 20px;

border: 2px solid #e9ecef;

border-radius: 15px;

font-size: 16px;

line-height: 1.8;

font-family: inherit;

resize: vertical;

transition: all 0.3s ease;

background: white;

}


.input-textarea.scroll-mode {

height: auto;

min-height: 400px;

resize: none;

}


.input-textarea:focus {

outline: none;

border-color: #667eea;

box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);

}


.preview-controls {

display: flex;

flex-direction: column;

gap: 10px;

position: absolute;

top: 20px;

right: 30px;

z-index: 20;

}


.control-row {

display: flex;

align-items: center;

gap: 15px;

}


.furigana-toggle {

background: #f6f7fb;

border: 1px solid #bbb;

border-radius: 20px;

padding: 6px 16px;

font-size: 14px;

box-shadow: 0 2px 8px rgba(0,0,0,0.06);

display: flex;

align-items: center;

gap: 8px;

user-select: none;

}


.furigana-toggle input[type="checkbox"] {

accent-color: #667eea;

width: 18px;

height: 18px;

}


.hide-furigana ruby rt {

display: none;

}


.mode-toggle {

display: flex;

background: #f8f9fa;

border-radius: 25px;

padding: 4px;

border: 2px solid #e9ecef;

}


.mode-btn {

padding: 8px 16px;

border: none;

background: transparent;

border-radius: 20px;

cursor: pointer;

font-size: 14px;

transition: all 0.3s ease;

color: #666;

}


.mode-btn.active {

background: linear-gradient(135deg, #667eea, #764ba2);

color: white;

box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);

}


.mode-btn:hover:not(.active) {

background: #e9ecef;

color: #333;

}


.output-container {

height: 400px;

padding: 20px;

border: 2px solid #e9ecef;

border-radius: 15px;

background: white;

font-size: 16px;

line-height: 2.2;

overflow-y: auto;

margin-top: 60px;

}


.output-container.scroll-mode {

height: auto;

min-height: 400px;

overflow-y: visible;

margin-top: 60px;

}


ruby {

position: relative;

}


rt {

font-size: 0.6em;

color: #666;

font-weight: normal;

}


.emphasis {

text-emphasis: filled circle;

text-emphasis-position: over right;

-webkit-text-emphasis: filled circle;

-webkit-text-emphasis-position: over right;

}


.stats {

display: grid;

grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

gap: 20px;

margin: 30px 20px;

}


.stat-card {

background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

color: white;

padding: 25px;

border-radius: 15px;

text-align: center;

box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);

transform: translateY(0);

transition: transform 0.3s ease;

}


.stat-card:hover {

transform: translateY(-5px);

}


.stat-number {

font-size: 2.5em;

font-weight: bold;

margin-bottom: 10px;

}


.stat-label {

font-size: 1.1em;

opacity: 0.9;

}


.back-to-top {

position: fixed;

bottom: 30px;

right: 30px;

width: 50px;

height: 50px;

background: linear-gradient(135deg, #667eea, #764ba2);

color: white;

border: none;

border-radius: 50%;

cursor: pointer;

font-size: 20px;

z-index: 1000;

box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);

transition: all 0.3s ease;

display: flex;

align-items: center;

justify-content: center;

}


.back-to-top:hover {

background: linear-gradient(135deg, #5a67d8, #6b46c1);

transform: translateY(-3px);

box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);

}


.back-to-top:active {

transform: translateY(-1px);

}


@media (max-width: 768px) {

.container {

flex-direction: column;

margin: 10px;

}


.resizer {

width: 100%;

height: 20px;

cursor: row-resize;

background: repeating-linear-gradient(

45deg,

#667eea 0 5px,

#fff 5px 10px

);

}


.resizer:hover {

background: repeating-linear-gradient(

45deg,

#ff6600 0 5px,

#fff 5px 10px

);

}


.resizer-grip {

width: 48px;

height: 8px;

}


.preview-controls {

position: relative;

top: 0;

right: 0;

margin-bottom: 20px;

flex-direction: row;

flex-wrap: wrap;

}


.control-row {

flex-direction: column;

gap: 10px;

}


.output-container {

margin-top: 0;

}


.output-container.scroll-mode {

margin-top: 0;

}


.header h1 {

font-size: 2em;

}


.stats {

grid-template-columns: 1fr 1fr;

gap: 15px;

margin: 20px 10px;

}


.back-to-top {

bottom: 20px;

right: 20px;

width: 45px;

height: 45px;

font-size: 18px;

}

}

</style>

</head>

<body>

<div class="header">

<h1>ふりがな付きテキストエディター</h1>

<p>小説や文章にふりがなを付けて、美しくレイアウトしましょう</p>

</div>


<div class="container">

<div class="panel input-panel" id="inputPanel">

<div class="section-title">テキスト入力</div>

<div class="instructions">

<h3>ふりがなの指定方法</h3>

<p><code>漢字かんじ</code> または <code>漢字(かんじ)</code></p>

<p><code>漢字かんじ</code> - 青空文庫形式(|で範囲を明示)</p>

<p><code></code> - 傍点(強調表示)</p>

<p>例: <code>うつくしいはないている。</code></p>

<p>例: <code>おつかれ様でした。紫陽花あじさい綺麗きれいです。</code></p>

<p>例: <code>な内容です。</code></p>

<p style="margin-top: 15px; font-size: 0.9em; color: #666;">

<strong>注意:</strong> このふりがなの付け方は青空文庫形式に近いですが、

<a href="https://www.neopage.com/announcements/1092527420616626176"

target="_blank"

style="color: #667eea; text-decoration: none;">

ネオページが提示する付け方

</a>

が基本となっています。

</p>

</div>

<textarea

id="inputText"

class="input-textarea"

placeholder="ここにふりがな指定のあるテキストを入力してください。&#10;&#10;例:&#10;うつくしいはないている。&#10;今日きょう天気てんきです。&#10;おつかれ様でした。紫陽花あじさい綺麗きれいです。&#10;これはな内容です。"

oninput="processText()"

></textarea>

</div>


<div class="resizer" id="resizer">

<div class="resizer-grip"></div>

</div>


<div class="panel" id="outputPanel">

<div class="section-title">プレビュー</div>

<div class="preview-controls">

<div class="control-row">

<div class="furigana-toggle">

<label>

<input type="checkbox" id="furiganaSwitch" checked>

ふりがなON/OFF

</label>

</div>

</div>

<div class="control-row">

<div class="mode-toggle">

<button class="mode-btn active" id="fixedModeBtn" onclick="setPreviewMode('fixed')">

固定式

</button>

<button class="mode-btn" id="scrollModeBtn" onclick="setPreviewMode('scroll')">

スクロール式

</button>

</div>

</div>

</div>

<div id="outputContainer" class="output-container">

<p style="color: #999; text-align: center; margin-top: 150px;">

左のテキストエリアに文章を入力すると、<br>

ふりがな付きでここに表示されます

</p>

</div>

</div>

</div>


<div class="stats">

<div class="stat-card">

<div class="stat-number" id="totalChars">0</div>

<div class="stat-label">総文字数</div>

</div>

<div class="stat-card">

<div class="stat-number" id="kanjiCount">0</div>

<div class="stat-label">漢字</div>

</div>

<div class="stat-card">

<div class="stat-number" id="kanaCount">0</div>

<div class="stat-label">仮名</div>

</div>

<div class="stat-card">

<div class="stat-number" id="otherCount">0</div>

<div class="stat-label">記号・その他</div>

</div>

</div>


<button class="back-to-top" id="backToTop" onclick="scrollToTop()" title="上に戻る">

</button>


<script>

function processText() {

const inputText = document.getElementById('inputText').value;

const outputContainer = document.getElementById('outputContainer');


// スクロール式の時にtextareaの高さを自動調整

adjustTextareaHeight();


if (!inputText.trim()) {

outputContainer.innerHTML = `

<p style="color: #999; text-align: center; margin-top: 150px;">

左のテキストエリアに文章を入力すると、<br>

ふりがな付きでここに表示されます

</p>

`;

updateStats(0, 0, 0, 0);

return;

}


let processedText = inputText;


// 傍点(例:花子迎えに来て)

processedText = processedText.replace(/(.?)(.+?)/g, (match, char, dots) => {

// char = 強調したい一文字, dots = 傍点範囲

// 例: 花子 → <span class="emphasis">駅まで</span>

// ※親文字をcharで区切る必要がなければdotsのみで良い

return `<span class="emphasis">${dots}</span>`;

});


// 漢字ふりがな or 漢字ふりがな

processedText = processedText.replace(/[|]([^\s(]+)《(.+?)/g, '<ruby>$1<rt>$2</rt></ruby>');


// |漢字(かな) or |漢字(かな) → ルビ化しない(そのまま表示)

processedText = processedText.replace(/[||]([^\s《(]+)\((.+?)\)/g, '$1($2)'); // 全角括弧に変換のみ


// |(かな) or |(かな) → ルビ化しない(そのまま表示)

processedText = processedText.replace(/[||]\((.+?)\)/g, '($1)'); // 全角括弧に変換のみ


// 漢字ふりがな

processedText = processedText.replace(/([一-龯々〆〇]+)《(.+?)》/g, '<ruby>$1<rt>$2</rt></ruby>');


// 漢字(かな) ただし直前が「|」の場合は既に処理済みなのでここは漢字の直後の(かな)のみ

processedText = processedText.replace(/([一-龯々〆〇]+)\(([\u3040-\u309F\u30A0-\u30FFー]+)\)/g, '<ruby>$1<rt>$2</rt></ruby>');


// |や|単体を除去(青空文庫対応)

processedText = processedText.replace(/[||]/g, '');


// 改行

processedText = processedText.replace(/\n/g, '<br>');


outputContainer.innerHTML = processedText;


// 文字数カウント

countCharacters(inputText);

}


// textareaの高さを自動調整

function adjustTextareaHeight() {

const textarea = document.getElementById('inputText');

const outputContainer = document.getElementById('outputContainer');


if (textarea.classList.contains('scroll-mode')) {

// スクロール式の場合、プレビューエリアの高さに合わせる

setTimeout(() => {

const outputHeight = outputContainer.scrollHeight;

const textareaMinHeight = Math.max(400, textarea.scrollHeight + 4);

const targetHeight = Math.max(textareaMinHeight, outputHeight);


textarea.style.height = targetHeight + 'px';

}, 50); // プレビューの描画完了を待つ

} else {

// 固定式の場合は元の高さに戻す

textarea.style.height = '400px';

}

}


function countCharacters(text) {

// ふりがな指定記号を除去してから文字数をカウント

let cleanText = text

.replace(/[一-龯々]+[あ-んア-ンー]+/g, function(match) {

return match.replace(/([一-龯々]+)[あ-んア-ンー]+/g, '$1');

}) // 漢字ふりがな形式から|とふりがな部分を除去

.replace(/|[一-龯々]+\([あ-んア-ンー]+\)/g, function(match) {

return match.replace(/|([一-龯々]+)\([あ-んア-ンー]+\)/g, '$1');

}) // |漢字(ふりがな)形式から|とふりがな部分を除去

.replace(/《[あ-んア-ンー]+》/g, '') // 《》記法のふりがな部分を除去

.replace(/\(([あ-んア-ンー]+)\)/g, '') // ()記法のふりがな部分を除去(ただし英数字の括弧は除去しない)

.replace(/[||]/g, '') // 残った|記号を除去

.replace(/[\s\n\r\t]/g, ''); // 空白、改行、タブを除去


let kanjiCount = 0;

let kanaCount = 0;

let otherCount = 0;


// より厳密な文字分類

for (let char of cleanText) {

if (/[一-龯々〆〇]/.test(char)) {

// 漢字(常用漢字、人名用漢字、その他の漢字)

kanjiCount++;

} else if (/[あ-んぁ-ゖゝゞ]/.test(char)) {

// ひらがな

kanaCount++;

} else if (/[ア-ンァ-ヺヽヾー]/.test(char)) {

// カタカナ

kanaCount++;

} else if (/[a-zA-Z]/.test(char)) {

// ローマ字

otherCount++;

} else if (/[0-90-9]/.test(char)) {

// 数字(半角・全角)

otherCount++;

} else if (/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~!"#$%&'()*+,-./:;<=>?@[¥]^_`{|}〜、。・「」『』【】〔〕…‥''""※→←↑↓∴∵≠≤≥∞∝∫∮Σ√∛∜±×÷°′″‰%‱℃℉]/.test(char)) {

// 記号・句読点

otherCount++;

}

// その他の文字(絵文字、特殊文字など)はカウントしない

}


const totalChars = kanjiCount + kanaCount + otherCount;

updateStats(totalChars, kanjiCount, kanaCount, otherCount);


// デバッグ用(実際の処理後テキストを確認)

console.log('処理後テキスト:', cleanText);

console.log('文字数:', totalChars, '漢字:', kanjiCount, 'かな:', kanaCount, 'その他:', otherCount);

}


function updateStats(total, kanji, kana, other) {

document.getElementById('totalChars').textContent = total.toLocaleString();

document.getElementById('kanjiCount').textContent = kanji.toLocaleString();

document.getElementById('kanaCount').textContent = kana.toLocaleString();

document.getElementById('otherCount').textContent = other.toLocaleString();

}


function setPreviewMode(mode) {

const outputContainer = document.getElementById('outputContainer');

const inputTextarea = document.getElementById('inputText');

const fixedBtn = document.getElementById('fixedModeBtn');

const scrollBtn = document.getElementById('scrollModeBtn');


if (mode === 'fixed') {

outputContainer.classList.remove('scroll-mode');

inputTextarea.classList.remove('scroll-mode');

fixedBtn.classList.add('active');

scrollBtn.classList.remove('active');

} else {

outputContainer.classList.add('scroll-mode');

inputTextarea.classList.add('scroll-mode');

fixedBtn.classList.remove('active');

scrollBtn.classList.add('active');

}


// モード切り替え後に高さを調整

adjustTextareaHeight();

}


// 上に戻る機能

function scrollToTop() {

window.scrollTo({

top: 0,

behavior: 'smooth'

});

}


// ふりがなON/OFF機能

function initializeFuriganaToggle() {

const furiganaSwitch = document.getElementById('furiganaSwitch');

const outputPanel = document.getElementById('outputPanel');


furiganaSwitch.addEventListener('change', function(e) {

if (e.target.checked) {

outputPanel.classList.remove('hide-furigana');

} else {

outputPanel.classList.add('hide-furigana');

}

});

}


// リサイザー機能

function initializeResizer() {

const resizer = document.getElementById('resizer');

const inputPanel = document.getElementById('inputPanel');

const outputPanel = document.getElementById('outputPanel');

let isResizing = false;

let lastX = 0;


// マウス操作

resizer.addEventListener('mousedown', function(e) {

isResizing = true;

lastX = e.clientX;

document.body.style.cursor = 'col-resize';

document.body.style.userSelect = 'none';

e.preventDefault();

});


document.addEventListener('mousemove', function(e) {

if (!isResizing) return;


const dx = e.clientX - lastX;

lastX = e.clientX;

const leftWidth = inputPanel.offsetWidth + dx;

const rightWidth = outputPanel.offsetWidth - dx;


// 最小幅120pxの制限

if (leftWidth > 120 && rightWidth > 120) {

inputPanel.style.flex = 'none';

outputPanel.style.flex = 'none';

inputPanel.style.width = leftWidth + 'px';

outputPanel.style.width = rightWidth + 'px';

}

});


document.addEventListener('mouseup', function() {

isResizing = false;

document.body.style.cursor = '';

document.body.style.userSelect = '';

});


// タッチ操作

resizer.addEventListener('touchstart', function(e) {

isResizing = true;

lastX = e.touches[0].clientX;

e.preventDefault();

});


document.addEventListener('touchmove', function(e) {

if (!isResizing) return;


const dx = e.touches[0].clientX - lastX;

lastX = e.touches[0].clientX;

const leftWidth = inputPanel.offsetWidth + dx;

const rightWidth = outputPanel.offsetWidth - dx;


if (leftWidth > 120 && rightWidth > 120) {

inputPanel.style.flex = 'none';

outputPanel.style.flex = 'none';

inputPanel.style.width = leftWidth + 'px';

outputPanel.style.width = rightWidth + 'px';

}

});


document.addEventListener('touchend', function() {

isResizing = false;

});

}


// 初期化時にサンプルテキストを設定

window.addEventListener('load', function() {

const sampleText = `うつくしいはないている。

今日きょう天気てんきです。

図書館としょかんほんみました。

つかれ様でした。紫陽花あじさい綺麗きれいです。

これはな内容です。`;


document.getElementById('inputText').value = sampleText;

processText();

initializeResizer();

initializeFuriganaToggle();


// textareaのinputイベントでも高さ調整

document.getElementById('inputText').addEventListener('input', adjustTextareaHeight);


// ウィンドウリサイズ時にも高さ調整

window.addEventListener('resize', adjustTextareaHeight);

});

</script>

</body>

</html>



(ここまでコピー)

------------------------------------------------



## HTMLファイル化の手順


### Step 1: コードのコピー

1. 上記のエディター画面で右クリック

2. 「すべて選択」をクリック

3. Ctrl+C(Mac: Cmd+C)でコピー


### Step 2: テキストエディターを開く

**Windows:**

- メモ帳を開く(スタートメニュー → アクセサリ → メモ帳)


**Mac:**

- テキストエディット を開く(アプリケーション → テキストエディット)


**その他:**

- VS Code、Notepad++、Atomなど任意のテキストエディター


### Step 3: ファイル作成

1. 新しい空白ファイルを作成

2. Ctrl+V(Mac: Cmd+V)でコードを貼り付け

3. 「名前を付けて保存」を選択

4. ファイル名を入力:furigana-editor.html

- 重要: 拡張子は必ず .html にしてください

5. 文字エンコードを「UTF-8」に設定(重要)

6. 保存


### Step 4: 使用開始

1. 保存したHTMLファイルをダブルクリック

2. ブラウザが自動で開いてエディターが起動

3. すぐに使用開始可能


### 配布時の注意事項

- スマホでの動作確認できてません

- インターネット接続不要(完全オフライン動作)

- 対応ブラウザ: Chrome、Firefox、Edge、Safari

- ファイルサイズ: 約15KB(軽量)


### 使用例

```

うつくしいはないている。

つかれ様でした。

これはな内容です。



皆さんの創作活動が、より楽しく充実したものになりますように。


この作品に、最初のコメントを書いてみませんか?