目次
ブックマーク
応援する
2
コメント
シェア
通報
Web小説を書くために使えるツールをhtmlで作成してみた。
Web小説を書くために使えるツールをhtmlで作成してみた。
城作也
文芸・その他ノンジャンル
2025年05月31日
公開日
2.3万字
連載中
ネオページで小説を書き、発表するために使えるツールをhtmlで作ったので紹介していきます。

ふりがな付きテキストエディター(プロトタイプ)

ネオページで小説を書いている皆さんこんにちは。


ふりがな付きテキストエディターを作りました。


オフラインでふりがな付きの文章を確認したいときに使えます。


文字数カウントも付けました。ネオページのカウントとほぼ近い文字数でカウントされます。(同じにするのが難しい……)


【主な機能】


○青空文庫形式のふりがな指定に対応 (作例を出すとふりがなとして表記されてしまうので載せられません)



○正確な文字数カウント

・漢字、ひらがな、カタカナ、記号を分類表示

・ふりがな指定記号は文字数に含まれません

・空白・改行も除外した正確なカウント


○使いやすいプレビュー機能

・リアルタイムでふりがな付き表示

・固定式/スクロール式の表示切り替え

・美しいレイアウトで読みやすい


【使い方】


1. 下のHTMLコードを全てコピー

2. メモ帳などのテキストエディターに貼り付け

3. 「○○.html」という名前で保存(○○は任意の名前)

4. 保存したファイルをダブルクリックでブラウザが開きます


【注意事項】


・PC専用です(スマートフォンでは正常に動作しません)

・インターネット接続不要でオフラインで使用可能

・Chrome、Firefox、Edgeなどの主要ブラウザで動作確認済み


【こんな方におすすめ】


・小説にふりがなを付けた状態を確認したい方

・正確な文字数を知りたい方

・美しいレイアウトでプレビューしたい方

・青空文庫形式にも対応したい方


ぜひ小説執筆にお役立てください。

バグなどのご報告もコメントをいただければ検討いたします。(できないことも多々あります)


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

(ここからコピーします)


<!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;

padding: 20px;

}


.container {

max-width: 1200px;

margin: 0 auto;

background: white;

border-radius: 20px;

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

overflow: hidden;

}


.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;

}


.main-content {

display: grid;

grid-template-columns: 1fr 4px 1fr;

gap: 0;

min-height: 600px;

position: relative;

}


.input-section {

padding: 30px;

background: #f8f9fa;

border-right: none;

}


.output-section {

padding: 30px;

background: white;

display: flex;

flex-direction: column;

}


.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;

}


.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:focus {

outline: none;

border-color: #667eea;

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

}


.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;

flex: none;

}


.output-container.scroll-mode {

height: auto;

min-height: 400px;

overflow-y: visible;

flex: 1;

}


.preview-controls {

display: flex;

align-items: center;

gap: 15px;

margin-bottom: 20px;

}


.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;

}


.stats {

display: grid;

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

gap: 20px;

margin-top: 30px;

padding: 0 30px 30px;

}


.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;

}


ruby {

position: relative;

}


rt {

font-size: 0.6em;

color: #666;

font-weight: normal;

}


.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;

}


@media (max-width: 768px) {

.main-content {

grid-template-columns: 1fr;

}


.input-section {

border-right: none;

border-bottom: 2px solid #e9ecef;

}


.header h1 {

font-size: 2em;

}


.stats {

grid-template-columns: 1fr 1fr;

gap: 15px;

}

}

</style>

</head>

<body>

<div class="container">

<div class="header">

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

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

</div>


<div class="main-content" id="mainContent">

<div class="input-section">

<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>

</div>

<textarea

id="inputText"

class="input-textarea"

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

oninput="processText()"

></textarea>

</div>


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

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

<div class="resizer-dots">

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

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

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

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

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

</div>

</div>


<div class="output-section">

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

<div class="preview-controls">

<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>

<span style="color: #666; font-size: 14px;" id="modeDescription">

固定高さでプレビューエリア内スクロール

</span>

</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>

</div>


<script>

function processText() {

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

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


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

.replace(/([一-龯々]+)([あ-んア-ンー]+)/g, '<ruby>$1<rt>$2</rt></ruby>') // 漢字かんじ形式

.replace(/([一-龯々]+)《([あ-んア-ンー]+)》/g, '<ruby>$1<rt>$2</rt></ruby>') // 漢字かんじ形式

.replace(/|([一-龯々]+)\(([あ-んア-ンー]+)\)/g, '<ruby>$1<rt>$2</rt></ruby>') // |漢字(かんじ)形式

.replace(/([一-龯々]+)\(([あ-んア-ンー]+)\)/g, '<ruby>$1<rt>$2</rt></ruby>'); // 漢字(かんじ)形式


// |と|を非表示にする(青空文庫形式対応)

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


// 改行をHTMLの改行に変換

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


outputContainer.innerHTML = processedText;


// 文字数カウント

countCharacters(inputText);

}


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();

}


// リサイザー機能

let isResizing = false;


function initializeResizer() {

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

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


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

isResizing = true;

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

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


// ドラッグ中のイベントリスナーを追加

document.addEventListener('mousemove', handleResize);

document.addEventListener('mouseup', stopResize);


e.preventDefault();

});


function handleResize(e) {

if (!isResizing) return;


const containerRect = mainContent.getBoundingClientRect();

const offsetX = e.clientX - containerRect.left;

const containerWidth = containerRect.width;


// 最小幅を20%、最大幅を80%に制限

const minWidth = containerWidth * 0.2;

const maxWidth = containerWidth * 0.8;


if (offsetX >= minWidth && offsetX <= maxWidth) {

const leftPercentage = (offsetX / containerWidth) * 100;

const rightPercentage = 100 - leftPercentage;


mainContent.style.gridTemplateColumns = `${leftPercentage}% 4px ${rightPercentage}%`;

}

}


function stopResize() {

isResizing = false;

document.body.style.cursor = '';

document.body.style.userSelect = '';


// イベントリスナーを削除

document.removeEventListener('mousemove', handleResize);

document.removeEventListener('mouseup', stopResize);

}

}


function setPreviewMode(mode) {

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

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

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

const description = document.getElementById('modeDescription');


if (mode === 'fixed') {

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

fixedBtn.classList.add('active');

scrollBtn.classList.remove('active');

description.textContent = '固定高さでプレビューエリア内スクロール';

} else {

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

fixedBtn.classList.remove('active');

scrollBtn.classList.add('active');

description.textContent = '内容に合わせて高さが変わります';

}

}


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

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

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

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

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

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


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

processText();

initializeResizer();

});

</script>

</body>

</html>

(ここまでコピーします)

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


【使用例】

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

つかれ様でした。


↓(こんな風にふりがな付きで表示されます)


美しい花が咲いている。

お疲れ様でした。



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