ネオページ作家の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>
<p><code>
<p><code>
<p>例: <code>
<p>例: <code>お
<p>例: <code>
<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="ここにふりがな指定のあるテキストを入力してください。 例:
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(/(.?)
// char = 強調したい一文字, dots = 傍点範囲
// 例: 花子
// ※親文字をcharで区切る必要がなければdotsのみで良い
return `<span class="emphasis">${dots}</span>`;
});
//
processedText = processedText.replace(/[|
// |漢字(かな) 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(/
return match.replace(/
}) //
.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(軽量)
### 使用例
```
お
これは
皆さんの創作活動が、より楽しく充実したものになりますように。