@ -0,0 +1,82 @@ | |||
import path from 'path'; | |||
import { AnkiConnect, IAnkiConnectActions } from '@/ankiconnect'; | |||
import { | |||
ensureDirSync, | |||
readFileSync, | |||
readdirSync, | |||
writeFileSync, | |||
} from 'fs-extra'; | |||
export class AnkiTemplateEditor { | |||
$connect = new AnkiConnect(); | |||
templateDir = 'template'; | |||
rootDir = path.resolve(this.templateDir, this.modelName); | |||
styleFilename = 'style.css'; | |||
constructor(public modelName: string) { | |||
ensureDirSync(this.rootDir); | |||
} | |||
async get() { | |||
const d = await this.$connect.api('modelTemplates', { | |||
modelName: this.modelName, | |||
}); | |||
Object.entries(d).map(([cardName, sideDict]) => { | |||
const folderDir = path.resolve(this.rootDir, cardName); | |||
ensureDirSync(folderDir); | |||
Object.entries(sideDict).map(([side, html]) => { | |||
writeFileSync(path.resolve(folderDir, side + '.html'), html); | |||
}); | |||
}); | |||
const s = await this.$connect.api('modelStyling', { | |||
modelName: this.modelName, | |||
}); | |||
writeFileSync(path.resolve(this.rootDir, this.styleFilename), s.css); | |||
} | |||
async set() { | |||
const templates: IAnkiConnectActions['updateModelTemplates']['params']['model']['templates'] = | |||
{}; | |||
for (const p of readdirSync(this.rootDir)) { | |||
if (p === this.styleFilename) { | |||
await this.$connect.api('updateModelStyling', { | |||
model: { | |||
name: this.modelName, | |||
css: readFileSync( | |||
path.resolve(this.rootDir, this.styleFilename), | |||
'utf-8', | |||
), | |||
}, | |||
}); | |||
continue; | |||
} | |||
readdirSync(path.resolve(this.rootDir, p)).map((p1) => { | |||
const side = p1.replace(/\.[^\.]+$/, '') as 'Front' | 'Back'; | |||
const sideMap = templates[p] || {}; | |||
sideMap[side] = readFileSync( | |||
path.resolve(this.rootDir, p, p1), | |||
'utf-8', | |||
); | |||
templates[p] = sideMap; | |||
}); | |||
} | |||
await this.$connect.api('updateModelTemplates', { | |||
model: { | |||
name: this.modelName, | |||
templates, | |||
}, | |||
}); | |||
} | |||
} | |||
if (require.main === module) { | |||
new AnkiTemplateEditor('jp.takoboto').set(); | |||
} |
@ -0,0 +1,209 @@ | |||
{{FrontSide}} | |||
<hr id=answer> | |||
{{#SentenceAudio}} | |||
{{SentenceAudio}} | |||
{{/SentenceAudio}} | |||
{{^SentenceAudio}} | |||
{{#Sentence}} | |||
[sound:https://peaceful-brushlands-36451.herokuapp.com/api/tts?lang=ja&is_sentence=true&q={{kanji:Sentence}}] | |||
{{/Sentence}} | |||
{{/SentenceAudio}} | |||
<p>{{furigana:Japanese}}</p> | |||
{{#JapaneseAlt}} | |||
<p>{{JapaneseAlt}}</p> | |||
{{/JapaneseAlt}} | |||
<p> | |||
{{#Pitch}} | |||
{{Pitch}} | |||
{{/Pitch}} | |||
{{^Pitch}} | |||
{{Reading}} | |||
{{/Pitch}} | |||
{{^JapaneseAudio}} | |||
<el-tts q="{{kanji:Japanese}}" /> | |||
{{/JapaneseAudio}} | |||
{{#JapaneseAudio}} | |||
<el-tts src="{{JapaneseAudio}}" /> | |||
{{/JapaneseAudio}} | |||
</p> | |||
{{#Sentence}} | |||
<p> | |||
<div> | |||
{{Sentence}} | |||
{{^SentenceAudio}} | |||
<el-tts q="{{kanji:Sentence}}" is-sentence /> | |||
{{/SentenceAudio}} | |||
{{#SentenceAudio}} | |||
<el-tts src="{{SentenceAudio}}" /> | |||
{{/SentenceAudio}} | |||
</div> | |||
{{SentenceMeaning}} | |||
</p> | |||
{{/Sentence}} | |||
<details> | |||
<summary>References</summary> | |||
{{#Takoboto}} | |||
<a href="{{Takoboto}}">Takoboto</a>・ | |||
{{/Takoboto}} | |||
{{#Akebi}} | |||
<a href="{{Akebi}}">Akebi</a>・ | |||
{{/Akebi}} | |||
<a class="ext-link" id="alc">ALC</a>・ | |||
<a class="ext-link" id="weblio">Weblio</a>・ | |||
<a class="ext-link" id="goo">Goo</a>・ | |||
<a class="ext-link" id="yourei">用例</a>・ | |||
<a class="ext-link" id="youglish">YouGlish</a>・ | |||
<a class="ext-link" id="ImmersionKit">ImmersionKit</a>・ | |||
<a href="https://peaceful-brushlands-36451.herokuapp.com?lang=ja&q={{kanji:Japanese}}">Audios</a> | |||
</details> | |||
{{#Mnemonic}} | |||
<details> | |||
<summary>Mnemonic</summary> | |||
<p>{{Mnemonic}}</p> | |||
</details> | |||
{{/Mnemonic}} | |||
<script type="module"> | |||
let linkEntries = {} | |||
try { linkEntries = JSON.parse('{{LinkEntries}}') } catch (e) { } | |||
document.querySelectorAll('a.ext-link').forEach(a => { | |||
switch (a.id) { | |||
case 'alc': | |||
a.href = `https://eow.alc.co.jp/search?q=${encodeURIComponent(linkEntries.alc || '{{kanji:Japanese}}')}` | |||
break | |||
case 'weblio': | |||
a.href = `https://www.weblio.jp/content/${encodeURIComponent(linkEntries.weblio || '{{kanji:Japanese}}')}` | |||
break | |||
case 'goo': | |||
a.href = `https://dictionary.goo.ne.jp/srch/all/${encodeURIComponent(linkEntries.goo || '{{kanji:Japanese}}')}/m1u/` | |||
break | |||
case 'yourei': | |||
a.href = `http://yourei.jp/${encodeURIComponent(linkEntries.yourei || '{{kanji:Japanese}}')}` | |||
break | |||
case 'youglish': | |||
a.href = `https://youglish.com/pronounce/${encodeURIComponent(linkEntries.youglish || '{{kanji:Japanese}}')}/japanese?` | |||
break | |||
case 'ImmersionKit': | |||
a.href = `https://v2.immersionkit.com/search?category=drama&keyword=${encodeURIComponent(linkEntries.ImmersionKit || '{{kanji:Japanese}}')}` | |||
break | |||
} | |||
}) | |||
</script> | |||
<script type="module"> | |||
function speak(q = '', lang = 'ja-JP') { | |||
if (window.AnkiDroidJS) { | |||
AnkiDroidJS.ankiTtsSetLanguage?.(lang) | |||
AnkiDroidJS.ankiTtsSpeak?.(q) | |||
} | |||
} | |||
class TTSElement extends HTMLElement { | |||
constructor() { | |||
super() | |||
this.attachShadow({ mode: 'open' }) | |||
this.wrapper = document.createElement('span') | |||
this.wrapper.innerText = '▶️' | |||
this.wrapper.style.cursor = 'pointer' | |||
this.shadowRoot.append(this.wrapper) | |||
} | |||
async connectedCallback() { | |||
let src = this.getAttribute('src') | |||
if (!src) { | |||
const q = this.getAttribute('q') | |||
const isSentence = this.hasAttribute('is-sentence') | |||
if (q) { | |||
src = `https://peaceful-brushlands-36451.herokuapp.com/api/tts?lang=ja&is_sentence=${isSentence}&q=${encodeURIComponent(q)}` | |||
} | |||
} | |||
const soundTag = { | |||
pre: '[sound:', | |||
post: ']' | |||
} | |||
if (src.startsWith(soundTag.pre) && src.endsWith(soundTag.post)) { | |||
src = src.substring(soundTag.pre, src.length - soundTag.post) | |||
} | |||
try { | |||
if (src) { | |||
const elAudio = document.createElement('audio') | |||
const setSrc = () => { | |||
const elSource = document.createElement('source') | |||
elSource.src = src | |||
elAudio.append(elSource) | |||
} | |||
elAudio.style.display = 'none' | |||
this.wrapper.append(elAudio) | |||
this.onclick = async () => { | |||
if (!elAudio.querySelector('source')) { | |||
setSrc() | |||
} | |||
elAudio.play() | |||
} | |||
} | |||
} catch (e) { | |||
console.error(e) | |||
this.onclick = () => { | |||
speak(q) | |||
} | |||
} | |||
} | |||
} | |||
try { | |||
customElements.define('el-tts', TTSElement) | |||
} catch (e) { } | |||
</script> | |||
<script type="module"> | |||
const removeHTML = (h) => { | |||
const div = document.createElement('div') | |||
div.innerHTML = h | |||
return div.innerText | |||
} | |||
const reJa = /[\p{sc=Han}\p{sc=Katakana}\p{sc=Hiragana}]/u | |||
document.querySelectorAll('.has-tts').forEach((el) => { | |||
const isSentence = el.classList.contains('is-sentence') | |||
const newEls = el.innerHTML.split(/<br *\/?>/g).map((t) => { | |||
t = removeHTML(t) | |||
const div = document.createElement('div') | |||
div.append(t) | |||
if (reJa.test(t)) { | |||
const elTTS = document.createElement('el-tts') | |||
elTTS.setAttribute('q', t) | |||
if (isSentence) elTTS.setAttribute('is-sentence', 'true') | |||
div.append(elTTS) | |||
} | |||
return div | |||
}) | |||
el.textContent = '' | |||
el.append(...newEls) | |||
}) | |||
</script> |
@ -0,0 +1,7 @@ | |||
{{^MeaningQuiz}} | |||
<p>{{Meaning}}</p> | |||
{{/MeaningQuiz}} | |||
{{#MeaningQuiz}} | |||
<p>{{MeaningQuiz}}</p> | |||
{{/MeaningQuiz}} |
@ -0,0 +1,197 @@ | |||
{{FrontSide}} | |||
<hr id=answer> | |||
{{#JapaneseAudio}} | |||
{{JapaneseAudio}} | |||
{{/JapaneseAudio}} | |||
{{^JapaneseAudio}} | |||
[sound:https://peaceful-brushlands-36451.herokuapp.com/api/tts?lang=ja&q={{kanji:Japanese}}] | |||
{{/JapaneseAudio}} | |||
<p>{{JapaneseAlt}}</p> | |||
<p> | |||
{{#Pitch}} | |||
{{Pitch}} | |||
{{/Pitch}} | |||
{{^Pitch}} | |||
{{Reading}} | |||
{{/Pitch}} | |||
</p> | |||
<p>{{Meaning}}</p> | |||
{{#Sentence}} | |||
<p> | |||
<div> | |||
{{furigana:Sentence}} | |||
{{^SentenceAudio}} | |||
<el-tts q="{{kanji:Sentence}}" is-sentence /> | |||
{{/SentenceAudio}} | |||
{{#SentenceAudio}} | |||
<el-tts src="{{SentenceAudio}}" /> | |||
{{/SentenceAudio}} | |||
</div> | |||
{{SentenceMeaning}} | |||
</p> | |||
{{/Sentence}} | |||
<details> | |||
<summary>References</summary> | |||
{{#Takoboto}} | |||
<a href="{{Takoboto}}">Takoboto</a>・ | |||
{{/Takoboto}} | |||
{{#Akebi}} | |||
<a href="{{Akebi}}">Akebi</a>・ | |||
{{/Akebi}} | |||
<a class="ext-link" id="alc">ALC</a>・ | |||
<a class="ext-link" id="weblio">Weblio</a>・ | |||
<a class="ext-link" id="goo">Goo</a>・ | |||
<a class="ext-link" id="yourei">用例</a>・ | |||
<a class="ext-link" id="youglish">YouGlish</a>・ | |||
<a class="ext-link" id="ImmersionKit">ImmersionKit</a>・ | |||
<a href="https://peaceful-brushlands-36451.herokuapp.com?lang=ja&q={{kanji:Japanese}}">Audios</a> | |||
</details> | |||
{{#Mnemonic}} | |||
<details> | |||
<summary>Mnemonic</summary> | |||
<p>{{Mnemonic}}</p> | |||
</details> | |||
{{/Mnemonic}} | |||
<script type="module"> | |||
let linkEntries = {} | |||
try { linkEntries = JSON.parse('{{LinkEntries}}') } catch (e) { } | |||
document.querySelectorAll('a.ext-link').forEach(a => { | |||
switch (a.id) { | |||
case 'alc': | |||
a.href = `https://eow.alc.co.jp/search?q=${encodeURIComponent(linkEntries.alc || '{{kanji:Japanese}}')}` | |||
break | |||
case 'weblio': | |||
a.href = `https://www.weblio.jp/content/${encodeURIComponent(linkEntries.weblio || '{{kanji:Japanese}}')}` | |||
break | |||
case 'goo': | |||
a.href = `https://dictionary.goo.ne.jp/srch/all/${encodeURIComponent(linkEntries.goo || '{{kanji:Japanese}}')}/m1u/` | |||
break | |||
case 'yourei': | |||
a.href = `http://yourei.jp/${encodeURIComponent(linkEntries.yourei || '{{kanji:Japanese}}')}` | |||
break | |||
case 'youglish': | |||
a.href = `https://youglish.com/pronounce/${encodeURIComponent(linkEntries.youglish || '{{kanji:Japanese}}')}/japanese?` | |||
break | |||
case 'ImmersionKit': | |||
a.href = `https://v2.immersionkit.com/search?category=drama&keyword=${encodeURIComponent(linkEntries.ImmersionKit || '{{kanji:Japanese}}')}` | |||
break | |||
} | |||
}) | |||
</script> | |||
<script type="module"> | |||
function speak(q = '', lang = 'ja-JP') { | |||
if (window.AnkiDroidJS) { | |||
AnkiDroidJS.ankiTtsSetLanguage?.(lang) | |||
AnkiDroidJS.ankiTtsSpeak?.(q) | |||
} | |||
} | |||
class TTSElement extends HTMLElement { | |||
constructor() { | |||
super() | |||
this.attachShadow({ mode: 'open' }) | |||
this.wrapper = document.createElement('span') | |||
this.wrapper.innerText = '▶️' | |||
this.wrapper.style.cursor = 'pointer' | |||
this.shadowRoot.append(this.wrapper) | |||
} | |||
async connectedCallback() { | |||
let src = this.getAttribute('src') | |||
if (!src) { | |||
const q = this.getAttribute('q') | |||
const isSentence = this.hasAttribute('is-sentence') | |||
if (q) { | |||
src = `https://peaceful-brushlands-36451.herokuapp.com/api/tts?lang=ja&is_sentence=${isSentence}=${encodeURIComponent(q)}` | |||
} | |||
} | |||
const soundTag = { | |||
pre: '[sound:', | |||
post: ']' | |||
} | |||
if (src.startsWith(soundTag.pre) && src.endsWith(soundTag.post)) { | |||
src = src.substring(soundTag.pre, src.length - soundTag.post) | |||
} | |||
try { | |||
if (src) { | |||
const elAudio = document.createElement('audio') | |||
const setSrc = () => { | |||
const elSource = document.createElement('source') | |||
elSource.src = src | |||
elAudio.append(elSource) | |||
} | |||
elAudio.style.display = 'none' | |||
this.wrapper.append(elAudio) | |||
this.onclick = async () => { | |||
if (!elAudio.querySelector('source')) { | |||
setSrc() | |||
} | |||
elAudio.play() | |||
} | |||
} | |||
} catch (e) { | |||
console.error(e) | |||
this.onclick = () => { | |||
speak(q) | |||
} | |||
} | |||
} | |||
} | |||
try { | |||
customElements.define('el-tts', TTSElement) | |||
} catch (e) { } | |||
</script> | |||
<script type="module"> | |||
const removeHTML = (h) => { | |||
const div = document.createElement('div') | |||
div.innerHTML = h | |||
return div.innerText | |||
} | |||
const reJa = /[\p{sc=Han}\p{sc=Katakana}\p{sc=Hiragana}]/u | |||
document.querySelectorAll('.has-tts').forEach((el) => { | |||
const isSentence = el.classList.contains('is-sentence') | |||
const newEls = el.innerHTML.split(/<br *\/?>/g).map((t) => { | |||
t = removeHTML(t) | |||
const div = document.createElement('div') | |||
div.append(t) | |||
if (reJa.test(t)) { | |||
const elTTS = document.createElement('el-tts') | |||
elTTS.setAttribute('q', t) | |||
if (isSentence) elTTS.setAttribute('is-sentence', 'true') | |||
div.append(elTTS) | |||
} | |||
return div | |||
}) | |||
el.textContent = '' | |||
el.append(...newEls) | |||
}) | |||
</script> |
@ -0,0 +1 @@ | |||
<p>{{furigana:Japanese}}</p> |
@ -0,0 +1,31 @@ | |||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP&display=swap'); | |||
.card { | |||
font-family: 'Noto Sans JP'; | |||
font-size: 20px; | |||
text-align: center; | |||
color: black; | |||
background-color: white; | |||
} | |||
.hidden { | |||
display: none; | |||
} | |||
.low-pitch { | |||
text-decoration: none; | |||
border-bottom: 3px dashed #512da8; | |||
} | |||
.high-pitch { | |||
font-weight: normal; | |||
border-left: 3px dashed #512da8; | |||
border-top: 3px dashed #512da8; | |||
border-right: 3px dashed #512da8; | |||
} | |||
.high-pitch-unterminated { | |||
font-weight: normal; | |||
border-left: 3px dashed #512da8; | |||
border-top: 3px dashed #512da8; | |||
} |