Created
March 3, 2020 16:13
-
-
Save gutchom/2b98b82f72f83fa38117c92813e034b5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Lyric Translation Builder</title> | |
<style> | |
body { | |
text-align: center; | |
} | |
h1 input { | |
border: none; | |
font-size: inherit; | |
font-weight: bold; | |
text-align: center; | |
} | |
rt.kana input { | |
width: 2em; | |
min-width: 2em; | |
max-width: 3em; | |
} | |
section.lyrics p { | |
font-size: 18px; | |
} | |
rt.kana, | |
rtc { | |
user-select: none; | |
} | |
rtc { | |
font-size: 75%; | |
} | |
rtc input { | |
width: 20em; | |
} | |
</style> | |
</head> | |
<body> | |
<article> | |
<h1 id="title"><input value="今宵の月のように"></h1> | |
<section id="lyrics" lang="ja-JP" translate="no"></section> | |
</article> | |
<section> | |
<textarea title="lyrics" id="textarea" cols="80" rows="20"> | |
くだらねえとつぶやいて | |
醒めたつらして歩く | |
いつの日か輝くだろう | |
あふれる熱い涙 | |
いつまでも続くのか | |
吐きすてて寝転んだ | |
俺もまた輝くだろう | |
今宵の月のようにAh… Ah… | |
夕暮れ過ぎて きらめく町の灯りは | |
悲しい色に 染まって揺れた | |
君がいつかくれた 思い出のかけら集めて | |
真夏の夜空 ひとり見上げた | |
新しい季節の始まりは | |
夏の風 町に吹くのさ | |
今日もまたどこへ行く | |
愛を探しに行こう | |
いつの日か輝くだろう | |
あふれる熱い涙 Ah… Ah… Oh yeah… | |
ポケットに手を つっこんで歩く | |
いつかの電車に乗って いつかの町まで | |
君のおもかげ きらりと光る 夜空に | |
涙も出ない 声も聞こえない | |
もう二度と戻らない日々を | |
俺たちは走り続ける | |
明日もまたどこへ行く | |
愛を探しに行こう | |
いつの日か輝くだろう | |
あふれる熱い涙 | |
明日もまたどこへ行く | |
愛を探しに行こう | |
見慣れてる町の空に | |
輝く月一つ | |
いつの日か輝くだろう | |
今宵の月のように Ah… Ah… | |
</textarea> | |
<button>決定</button> | |
</section> | |
<script> | |
const press = {} | |
window.addEventListener('keydown', handleKeyDown) | |
window.addEventListener('keyup', handleKeyUp) | |
function handleKeyDown(e) { | |
press[e.key] = true | |
} | |
function handleKeyUp(e) { | |
press[e.key] = false | |
} | |
document.querySelector('button').onclick = function(e) { | |
document.getElementById('lyrics').innerHTML = parse(e.target.previousElementSibling.value) | |
document.querySelectorAll('rb').forEach(el => el.onclick = handleRbClick) | |
document.querySelectorAll('rt').forEach(el => el.onclick = handleRtClick) | |
document.querySelectorAll('ruby.sentence').forEach(el => el.onclick = handleSentenceClick) | |
e.target.parentElement.remove() | |
} | |
function handleRbClick(e) { | |
e.stopPropagation() | |
appendInput(e.target.nextElementSibling) | |
} | |
function handleRtClick(e) { | |
e.stopPropagation() | |
appendInput(e.target) | |
} | |
// TODO: 現在開いている行以降の行をクリックした際に一旦入力欄が閉じる挙動を、連続的にフォーカスが映るようにする | |
function handleSentenceClick(e) { | |
appendInput(e.currentTarget.lastElementChild) | |
} | |
function appendInput(element) { | |
const input = document.createElement('input') | |
input.value = element.innerText | |
element.innerHTML = '' | |
element.appendChild(input) | |
input.onblur = handleInputBlur | |
input.onkeydown = handleInputKeyDown | |
input.onclick = (e) => e.stopPropagation() | |
input.focus() | |
} | |
function handleInputBlur(e) { | |
e.target.parentElement.innerHTML = e.target.value | |
} | |
function handleInputKeyDown(e) { | |
switch (e.key) { | |
case 'Escape': | |
e.target.blur() | |
break | |
case 'Enter': | |
e.preventDefault() | |
/* | |
case 'Enter': | |
const sentence = find(e.target, el => el.classList.contains('sentence')) | |
appendInput(e.target.parentElement.tagName === 'RTC' ? sentence.nextElementSibling.lastElementChild : sentence.lastElementChild) | |
break | |
*/ | |
case 'Tab': | |
e.preventDefault() | |
const elements = [...document.querySelectorAll('#lyrics rt')] | |
const index = elements.indexOf(e.currentTarget.parentElement) + (press['Shift'] ? -1 : 1) | |
if (-1 < index && index < elements.length) { | |
appendInput(elements[index]) | |
} | |
break | |
} | |
} | |
function find(element, test) { | |
if (test(element)) { | |
return element | |
} else if (element.parentElement) { | |
return find(element.parentElement, test) | |
} | |
} | |
function parse(text) { | |
const markup = (head, tail, middle) => pattern => { | |
let last = false | |
return char => [head + char, char, middle || char, tail + char][last * 2 - (last = pattern.test(char)) + 1] | |
} | |
const kanji = markup('<ruby><rb>', '</rb><rt class="kana"></rt></ruby>')(/(?:[々〇〻\u2E80-\u2FDF\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]|[\uD840-\uD87F][\uDC00-\uDFFF])/) | |
const block = markup('<br>\n<rtc lang="en-US"></rtc>\n</ruby>\n<br>', '<ruby class="sentence">\n', '</p>\n<p>')(/\n/) | |
return text.split('').reduce((markup, char) => markup + block(kanji(char)), '\n<p>\n<ruby class="sentence">\n') + '</p>\n' | |
} | |
function getSelectionText(lang) { | |
const frag = window.getSelection().getRangeAt(0).cloneContents() | |
const translations = frag.querySelectorAll(`rtc:lang(${lang})`) | |
console.log(translations) | |
return [...(translations.length > 0 ? translations : removeByQuery(frag , 'rtc', lang === 'kana' ? 'rb' : 'rt.kana').children)].map(el => el.textContent.replace(/\s{2,}/g, '')).filter(sentence => sentence.length > 0).join('\n') | |
function removeByQuery(fragment, ...selectors) { | |
selectors.forEach(selector => fragment.querySelectorAll(selector).forEach(el => el.remove())) | |
return fragment | |
} | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment