|
<!DOCTYPE html> |
|
<html lang="ja" data-bs-theme="dark"> |
|
|
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>テキスト編集ユーティリティ</title> |
|
|
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> |
|
<style> |
|
.text-area-container { |
|
margin: 20px 0; |
|
position: relative; |
|
} |
|
|
|
.text-area-controls { |
|
position: absolute; |
|
top: 10px; |
|
right: 10px; |
|
z-index: 10; |
|
display: flex; |
|
gap: 5px; |
|
} |
|
|
|
.text-area-controls .btn { |
|
padding: 4px 8px; |
|
font-size: 12px; |
|
border-radius: 4px; |
|
} |
|
|
|
.text-area-container textarea { |
|
padding-right: 80px; |
|
} |
|
|
|
.accordion-button:not(.collapsed) { |
|
background-color: var(--bs-primary-bg-subtle); |
|
} |
|
|
|
.layout-wrapper { |
|
display: flex; |
|
flex-direction: row; |
|
height: 100vh; |
|
} |
|
|
|
.sidebar { |
|
width: 250px; |
|
min-width: 250px; |
|
transition: all 0.3s; |
|
z-index: 1000; |
|
background-color: var(--bs-body-bg); |
|
border-right: 1px solid var(--bs-border-color, #444); |
|
margin-top: 32px; |
|
} |
|
|
|
.main-content { |
|
flex: 1 1 0%; |
|
transition: all 0.3s; |
|
margin-top: 32px; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: stretch; |
|
} |
|
|
|
.main-inner { |
|
width: 100%; |
|
max-width: 100%; |
|
padding: 0 8px; |
|
margin: 0; |
|
} |
|
|
|
@media (min-width: 768px) { |
|
.layout-wrapper { |
|
padding: 0 25vh; |
|
} |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<div class="layout-wrapper"> |
|
|
|
|
|
<div class="main-content" id="mainContent"> |
|
<div class="main-inner"> |
|
<h2 class="mb-3">テキスト編集ユーティリティ</h2> |
|
<div class="d-grid gap-2"> |
|
<button class="btn btn-primary" id="processBtn"> |
|
<i class="fas fa-cog me-2"></i>Process |
|
</button> |
|
<button class="btn btn-secondary" id="deprocessBtn"> |
|
<i class="fas fa-undo me-2"></i>Deprocess |
|
</button> |
|
</div> |
|
<div class="accordion" id="textEditorAccordion"> |
|
|
|
<div class="accordion-item"> |
|
<h2 class="accordion-header"> |
|
<button class="accordion-button" type="button" data-bs-toggle="collapse" |
|
data-bs-target="#collapseOne"> |
|
<i class="fas fa-chevron-down me-2"></i>上部テキストエリア |
|
</button> |
|
</h2> |
|
<div id="collapseOne" class="accordion-collapse collapse show" |
|
data-bs-parent="#textEditorAccordion"> |
|
<div class="accordion-body"> |
|
<div class="text-area-container"> |
|
<div class="text-area-controls"> |
|
<button class="btn btn-outline-primary btn-sm" onclick="copyToClipboard('topText', event)" title="コピー"> |
|
<i class="fas fa-copy"></i> |
|
</button> |
|
<button class="btn btn-outline-secondary btn-sm" onclick="pasteFromClipboard('topText')" title="ペースト"> |
|
<i class="fas fa-paste"></i> |
|
</button> |
|
</div> |
|
<textarea id="topText" class="form-control" style="width:100%; min-height:50vh;" |
|
rows="10" placeholder="ここにテキストを入力してください"></textarea> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="accordion-item"> |
|
<h2 class="accordion-header"> |
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" |
|
data-bs-target="#collapseTwo"> |
|
<i class="fas fa-chevron-down me-2"></i>下部テキストエリア |
|
</button> |
|
</h2> |
|
<div id="collapseTwo" class="accordion-collapse collapse" data-bs-parent="#textEditorAccordion"> |
|
<div class="accordion-body"> |
|
<div class="text-area-container"> |
|
<div class="text-area-controls"> |
|
<button class="btn btn-outline-primary btn-sm" onclick="copyToClipboard('bottomText', event)" title="コピー"> |
|
<i class="fas fa-copy"></i> |
|
</button> |
|
<button class="btn btn-outline-secondary btn-sm" onclick="pasteFromClipboard('bottomText')" title="ペースト"> |
|
<i class="fas fa-paste"></i> |
|
</button> |
|
</div> |
|
<textarea id="bottomText" class="form-control" style="width:100%; min-height:50vh;" |
|
rows="10" placeholder="ここにテキストを入力してください"></textarea> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="accordion-item"> |
|
<h2 class="accordion-header"> |
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" |
|
data-bs-target="#collapseThree"> |
|
<i class="fas fa-chevron-down me-2"></i>メモ |
|
</button> |
|
</h2> |
|
<div id="collapseThree" class="accordion-collapse collapse" |
|
data-bs-parent="#textEditorAccordion"> |
|
<div class="accordion-body"> |
|
<div class="text-area-container"> |
|
<div class="text-area-controls"> |
|
<button class="btn btn-outline-primary btn-sm" onclick="copyToClipboard('memoArea', event)" title="コピー"> |
|
<i class="fas fa-copy"></i> |
|
</button> |
|
<button class="btn btn-outline-secondary btn-sm" onclick="pasteFromClipboard('memoArea')" title="ペースト"> |
|
<i class="fas fa-paste"></i> |
|
</button> |
|
</div> |
|
<textarea id="memoArea" class="form-control" style="width:100%; min-height:50vh;" |
|
rows="10" placeholder="ここにテキストを入力してください"></textarea> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> |
|
<script> |
|
let lastSaveTimestamp = 0; |
|
|
|
|
|
const ZERO_WIDTH_SPACE = '‌'; |
|
|
|
|
|
const KNOWN_DUMMY_CHARS = [ |
|
'\u200c', '\u200b', '\u200d', |
|
'‌', '​', '‌', |
|
'\u200e', '\u200f', |
|
'\u2060', '\u2061', '\u2062', '\u2063', '\u2064' |
|
]; |
|
|
|
|
|
function analyzeCharFrequency(text) { |
|
const frequency = new Map(); |
|
for (let i = 0; i < text.length; i++) { |
|
const char = text[i]; |
|
frequency.set(char, (frequency.get(char) || 0) + 1); |
|
} |
|
return frequency; |
|
} |
|
|
|
|
|
function findPatternCandidate(text) { |
|
if (!text || text.length < 3) return null; |
|
|
|
|
|
const frequency = analyzeCharFrequency(text); |
|
|
|
|
|
const patternMap = new Map(); |
|
for (let i = 1; i < text.length - 1; i += 2) { |
|
const char = text[i]; |
|
const prevChar = text[i - 1]; |
|
const nextChar = text[i + 1]; |
|
|
|
|
|
if (prevChar === nextChar && |
|
frequency.get(char) > text.length * 0.3) { |
|
patternMap.set(char, (patternMap.get(char) || 0) + 1); |
|
} |
|
} |
|
|
|
|
|
let maxCount = 0; |
|
let candidate = null; |
|
for (const [char, count] of patternMap) { |
|
if (count > maxCount) { |
|
maxCount = count; |
|
candidate = char; |
|
} |
|
} |
|
|
|
return candidate; |
|
} |
|
|
|
|
|
function detectDummyChar(text) { |
|
if (!text || text.length < 3) return null; |
|
|
|
|
|
for (let i = 1; i < text.length - 1; i += 2) { |
|
const char = text[i]; |
|
if (KNOWN_DUMMY_CHARS.includes(char) && |
|
text[i - 1] !== char && text[i + 1] !== char) { |
|
return char; |
|
} |
|
} |
|
|
|
|
|
return findPatternCandidate(text); |
|
} |
|
|
|
|
|
function insertBetweenChars(text, insertStr) { |
|
if (!text) return ''; |
|
return text.split('').join(insertStr); |
|
} |
|
|
|
|
|
function removeBetweenChars(text, removeStr) { |
|
if (!text) return ''; |
|
return text.split(removeStr).join(''); |
|
} |
|
|
|
|
|
function decodeHtmlEntities(str) { |
|
const textarea = document.createElement('textarea'); |
|
textarea.innerHTML = str; |
|
return textarea.value; |
|
} |
|
|
|
|
|
function getUpperText() { |
|
return document.querySelectorAll('.text-area-container textarea')[0].value; |
|
} |
|
function setUpperText(val) { |
|
document.querySelectorAll('.text-area-container textarea')[0].value = val; |
|
} |
|
function getLowerText() { |
|
return document.querySelectorAll('.text-area-container textarea')[1].value; |
|
} |
|
function setLowerText(val) { |
|
document.querySelectorAll('.text-area-container textarea')[1].value = val; |
|
} |
|
|
|
|
|
document.getElementById('processBtn').addEventListener('click', function () { |
|
const upperText = getUpperText(); |
|
const processed = insertBetweenChars(upperText, ZERO_WIDTH_SPACE); |
|
setLowerText(processed); |
|
|
|
const lowerAccordion = new bootstrap.Collapse(document.getElementById('collapseTwo'), { |
|
toggle: false |
|
}); |
|
lowerAccordion.show(); |
|
}); |
|
|
|
|
|
document.getElementById('deprocessBtn').addEventListener('click', function () { |
|
let lowerText = getLowerText(); |
|
|
|
lowerText = decodeHtmlEntities(lowerText); |
|
const dummyChar = detectDummyChar(lowerText); |
|
if (!dummyChar) { |
|
alert('文字間のダミー文字を検出できませんでした。'); |
|
return; |
|
} |
|
const deprocessed = removeBetweenChars(lowerText, dummyChar); |
|
setUpperText(deprocessed); |
|
|
|
const upperAccordion = new bootstrap.Collapse(document.getElementById('collapseOne'), { |
|
toggle: false |
|
}); |
|
upperAccordion.show(); |
|
}); |
|
|
|
function saveToUserStorage(force = false) { |
|
const currentTime = Date.now(); |
|
if (currentTime - lastSaveTimestamp < 5000 && !force) { |
|
console.debug('セーブをスキップします'); |
|
return; |
|
} |
|
console.debug('セーブを実行します'); |
|
|
|
|
|
const textUtilData = JSON.parse(localStorage.getItem('textUtil') || '{}'); |
|
|
|
const newData = {}; |
|
Array.from(document.querySelectorAll("input[id], textarea[id], select[id]")).forEach(el => { |
|
if (el.id) { |
|
newData[el.id] = el.type === 'checkbox' ? el.checked : el.value; |
|
} |
|
}); |
|
Object.assign(textUtilData, newData); |
|
console.log(textUtilData); |
|
localStorage.setItem('textUtil', JSON.stringify(textUtilData)); |
|
lastSaveTimestamp = currentTime; |
|
} |
|
|
|
function loadFromUserStorage() { |
|
const textUtilData = JSON.parse(localStorage.getItem('textUtil') || '{}'); |
|
document.getElementById('bottomText').value = textUtilData['bottomText'] || ''; |
|
document.getElementById('topText').value = textUtilData['topText'] || ''; |
|
document.getElementById('memoArea').value = textUtilData['memoArea'] || ''; |
|
} |
|
|
|
document.querySelectorAll("#bottomText, #topText").forEach(el => { |
|
el.addEventListener('input', () => { |
|
saveToUserStorage(false); |
|
}); |
|
}); |
|
document.querySelectorAll("#memoArea").forEach(el => { |
|
el.addEventListener('input', () => { |
|
saveToUserStorage(true); |
|
}); |
|
}); |
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
|
loadFromUserStorage(); |
|
}); |
|
|
|
|
|
async function copyToClipboard(textareaId, event) { |
|
const textarea = document.getElementById(textareaId); |
|
const text = textarea.value; |
|
|
|
try { |
|
await navigator.clipboard.writeText(text); |
|
|
|
const button = event.target.closest('button'); |
|
const originalText = button.innerHTML; |
|
button.innerHTML = '<i class="fas fa-check"></i>'; |
|
setTimeout(() => { |
|
button.innerHTML = originalText; |
|
}, 1000); |
|
} catch (err) { |
|
console.error('クリップボードへのコピーに失敗しました:', err); |
|
alert('クリップボードへのコピーに失敗しました'); |
|
} |
|
} |
|
|
|
|
|
async function pasteFromClipboard(textareaId) { |
|
const textarea = document.getElementById(textareaId); |
|
|
|
try { |
|
const text = await navigator.clipboard.readText(); |
|
textarea.value = text; |
|
|
|
saveToUserStorage(true); |
|
} catch (err) { |
|
console.error('クリップボードからのペーストに失敗しました:', err); |
|
alert('クリップボードからのペーストに失敗しました'); |
|
} |
|
} |
|
|
|
|
|
</script> |
|
</body> |
|
|
|
</html> |