diff options
author | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2020-03-01 14:15:28 -0500 |
---|---|---|
committer | toasted-nutbread <toasted-nutbread@users.noreply.github.com> | 2020-03-28 10:25:57 -0400 |
commit | 97a520cc1595369dc18ddcf74ab7f0ba4e03f55b (patch) | |
tree | 9ee58cba18c0e4f60571ab6266a91b3458633a3e | |
parent | 2d7214ce60ef55b0d3d6e98b86f41b4e9fce5c48 (diff) |
Add support for displaying pitch accents
-rw-r--r-- | ext/mixed/css/display-dark.css | 8 | ||||
-rw-r--r-- | ext/mixed/css/display-default.css | 8 | ||||
-rw-r--r-- | ext/mixed/css/display.css | 101 | ||||
-rw-r--r-- | ext/mixed/display-templates.html | 6 | ||||
-rw-r--r-- | ext/mixed/js/display-generator.js | 159 |
5 files changed, 277 insertions, 5 deletions
diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css index 908d9cc5..dc344099 100644 --- a/ext/mixed/css/display-dark.css +++ b/ext/mixed/css/display-dark.css @@ -59,12 +59,14 @@ h2 { border-bottom-color: #2f2f2f; } color: #666666; } -.term-definition-container, -.kanji-glossary-container { +.term-definition-list, +.term-pitch-accent-group-list, +.kanji-glossary-list { color: #888888; } .term-glossary, +.term-pitch-accent, .kanji-glossary { color: #d4d4d4; } @@ -74,3 +76,5 @@ h2 { border-bottom-color: #2f2f2f; } background-color: #d4d4d4; color: #1e1e1e; } + +.term-pitch-accent-character:before { border-color: #ffffff; } diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css index e43e3742..81623ebc 100644 --- a/ext/mixed/css/display-default.css +++ b/ext/mixed/css/display-default.css @@ -59,12 +59,14 @@ h2 { border-bottom-color: #eeeeee; } color: #999999; } -.term-definition-container, -.kanji-glossary-container { +.term-definition-list, +.term-pitch-accent-group-list, +.kanji-glossary-list { color: #777777; } .term-glossary, +.term-pitch-accent, .kanji-glossary { color: #000000; } @@ -74,3 +76,5 @@ h2 { border-bottom-color: #eeeeee; } background-color: #333333; color: #ffffff; } + +.term-pitch-accent-character:before { border-color: #000000; } diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index 51015057..0a1ba658 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -437,6 +437,107 @@ button.action-button { /* + * Pitch accent styles + */ + +.term-pitch-accent-group-list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.term-pitch-accent-group-list:not([data-count="0"]):not([data-count="1"]) { + padding-left: 1.4em; + list-style-type: decimal; +} + +.term-pitch-accent-list { + margin: 0; + padding: 0; + list-style-type: none; + display: inline; +} + +.term-pitch-accent-list:not([data-count="0"]):not([data-count="1"]) { + padding-left: 1.4em; + list-style-type: circle; + display: block; +} + +.term-pitch-accent { + display: inline; + line-height: 1.5em; +} + +.term-pitch-accent-list:not([data-count="0"]):not([data-count="1"])>.term-pitch-accent { + display: list-item; +} + +.term-pitch-accent-group-tag-list { + margin-right: 0.375em; +} +.entry[data-unique-expression-count="1"] .term-pitch-accent-expression-list { + display: none; +} +.term-pitch-accent-expression:not(:last-of-type):after { + content: "\3001"; +} +.term-pitch-accent-expression:last-of-type:after { + content: "\FF1A"; +} + +.term-pitch-accent-tag-list:not([data-count="0"]) { + margin-right: 0.375em; +} + +.term-special-tags>.pitches { + display: inline; +} + +.term-pitch-accent-character { + display: inline-block; + position: relative; +} +.term-pitch-accent-character[data-pitch='high']:before { + content: ""; + display: block; + user-select: none; + pointer-events: none; + position: absolute; + top: 0.1em; + left: 0; + right: 0; + height: 0; + border-top-width: 0.1em; + border-top-style: solid; +} +.term-pitch-accent-character[data-pitch='high'][data-pitch-next='low']:before { + right: -0.1em; + height: 0.4em; + border-right-width: 0.1em; + border-right-style: solid; +} +.term-pitch-accent-character[data-pitch='high'][data-pitch-next='low'] { + padding-right: 0.1em; + margin-right: 0.1em; +} + +.term-pitch-accent-position:before { + content: " ["; +} +.term-pitch-accent-position:after { + content: "]"; +} + +.term-pitch-accent-details { + display: inline-block; + height: 0; + padding: 0 0.25em; + vertical-align: middle; +} + + +/* * Kanji */ diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html index 837245cf..c6f208a8 100644 --- a/ext/mixed/display-templates.html +++ b/ext/mixed/display-templates.html @@ -19,6 +19,7 @@ </div> <div class="term-entry-body"> <div class="term-entry-body-section term-pitch-accent-container"><h2 class="term-entry-body-section-header term-pitch-accent-header">Pitch Accents</h2><ol class="term-entry-body-section-content term-pitch-accent-group-list"></ol></div> + <div class="term-entry-body-section term-definition-container"><h2 class="term-entry-body-section-header term-definition-header">Definitions</h2><ol class="term-entry-body-section-content term-definition-list"></ol></div> </div> <pre class="debug-info"></pre> </div></template> @@ -36,6 +37,11 @@ <template id="term-glossary-item-template"><li class="term-glossary-item"><span class="term-glossary-separator"> </span><span class="term-glossary"></span></li></template> <template id="term-reason-template"><span class="term-reason"></span><span class="term-reason-separator"> </span></template> +<template id="term-pitch-accent-group-template"><li class="term-pitch-accent-group"><span class="term-pitch-accent-group-tag-list tag-list"></span><ul class="term-pitch-accent-list"></ul></li></template> +<template id="term-pitch-accent-expression-template"><span class="term-pitch-accent-expression"></span></template> +<template id="term-pitch-accent-template"><li class="term-pitch-accent"><span class="term-pitch-accent-tag-list tag-list"></span><span class="term-pitch-accent-expression-list"></span><span class="term-pitch-accent-characters"></span><span class="term-pitch-accent-position"></span></li></template> +<template id="term-pitch-accent-character-template"><span class="term-pitch-accent-character"><span class="term-pitch-accent-character-inner"></span></span></template> + <template id="kanji-entry-template"><div class="entry" data-type="kanji"> <div class="entry-header1"> <div class="entry-header2"> diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js index fd7c5c1f..449bac1d 100644 --- a/ext/mixed/js/display-generator.js +++ b/ext/mixed/js/display-generator.js @@ -37,9 +37,14 @@ class DisplayGenerator { const expressionsContainer = node.querySelector('.term-expression-list'); const reasonsContainer = node.querySelector('.term-reasons'); + const pitchesContainer = node.querySelector('.term-pitch-accent-group-list'); const frequenciesContainer = node.querySelector('.frequencies'); const definitionsContainer = node.querySelector('.term-definition-list'); const debugInfoContainer = node.querySelector('.debug-info'); + const bodyContainer = node.querySelector('.term-entry-body'); + + const pitches = DisplayGenerator._getPitchInfos(details); + const pitchCount = pitches.reduce((i, v) => i + v[1].length, 0); const expressionMulti = Array.isArray(details.expressions); const definitionMulti = Array.isArray(details.definitions); @@ -52,9 +57,12 @@ class DisplayGenerator { node.dataset.expressionCount = `${expressionCount}`; node.dataset.definitionCount = `${definitionCount}`; node.dataset.uniqueExpressionCount = `${uniqueExpressionCount}`; + node.dataset.pitchAccentDictionaryCount = `${pitches.length}`; + node.dataset.pitchAccentCount = `${pitchCount}`; bodyContainer.dataset.sectionCount = `${ - (definitionCount > 0 ? 1 : 0) + (definitionCount > 0 ? 1 : 0) + + (pitches.length > 0 ? 1 : 0) }`; const termTags = details.termTags; @@ -64,6 +72,7 @@ class DisplayGenerator { DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), expressions, [[details, termTags]]); DisplayGenerator._appendMultiple(reasonsContainer, this.createTermReason.bind(this), details.reasons); DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies); + DisplayGenerator._appendMultiple(pitchesContainer, this.createPitches.bind(this), pitches); DisplayGenerator._appendMultiple(definitionsContainer, this.createTermDefinitionItem.bind(this), details.definitions, [details]); if (debugInfoContainer !== null) { @@ -270,6 +279,73 @@ class DisplayGenerator { return node; } + createPitches(details) { + if (!this._termPitchAccentStaticTemplateIsSetup) { + this._termPitchAccentStaticTemplateIsSetup = true; + const t = this._templateHandler.instantiate('term-pitch-accent-static'); + document.head.appendChild(t); + } + + const [dictionary, dictionaryPitches] = details; + + const node = this._templateHandler.instantiate('term-pitch-accent-group'); + node.dataset.dictionary = dictionary; + node.dataset.pitchesMulti = 'true'; + node.dataset.pitchesCount = `${dictionaryPitches.length}`; + + const tag = this.createTag({notes: '', name: dictionary, category: 'dictionary'}); + node.querySelector('.term-pitch-accent-group-tag-list').appendChild(tag); + + const n = node.querySelector('.term-pitch-accent-list'); + DisplayGenerator._appendMultiple(n, this.createPitch.bind(this), dictionaryPitches); + + return node; + } + + createPitch(details) { + const {expressions, reading, position, tags} = details; + const morae = DisplayGenerator._jpGetKanaMorae(reading); + + const node = this._templateHandler.instantiate('term-pitch-accent'); + + node.dataset.pitchAccentPosition = `${position}`; + node.dataset.tagCount = `${tags.length}`; + + let n = node.querySelector('.term-pitch-accent-position'); + n.textContent = `${position}`; + + n = node.querySelector('.term-pitch-accent-tag-list'); + DisplayGenerator._appendMultiple(n, this.createTag.bind(this), tags); + + n = node.querySelector('.term-pitch-accent-expression-list'); + DisplayGenerator._appendMultiple(n, this.createPitchExpression.bind(this), expressions); + + n = node.querySelector('.term-pitch-accent-characters'); + for (let i = 0, ii = morae.length; i < ii; ++i) { + const mora = morae[i]; + const highPitch = DisplayGenerator._jpIsMoraPitchHigh(i, position); + const highPitchNext = DisplayGenerator._jpIsMoraPitchHigh(i + 1, position); + + const n1 = this._templateHandler.instantiate('term-pitch-accent-character'); + const n2 = n1.querySelector('.term-pitch-accent-character-inner'); + + n1.dataset.position = `${i}`; + n1.dataset.pitch = highPitch ? 'high' : 'low'; + n1.dataset.pitchNext = highPitchNext ? 'high' : 'low'; + n2.textContent = mora; + + n.appendChild(n1); + } + + return node; + } + + createPitchExpression(expression) { + const node = this._templateHandler.instantiate('term-pitch-accent-expression'); + node.textContent = expression; + return node; + } + createFrequencyTag(details) { const node = this._templateHandler.instantiate('tag-frequency'); @@ -356,4 +432,85 @@ class DisplayGenerator { container.appendChild(document.createTextNode(parts[i])); } } + + static _getPitchInfos(definition) { + const results = new Map(); + + const expressions = definition.expressions; + const sources = Array.isArray(expressions) ? expressions : [definition]; + for (const {pitches: expressionPitches, expression} of sources) { + for (const {reading, pitches, dictionary} of expressionPitches) { + let dictionaryResults = results.get(dictionary); + if (typeof dictionaryResults === 'undefined') { + dictionaryResults = []; + results.set(dictionary, dictionaryResults); + } + + for (const {position, tags} of pitches) { + let pitchInfo = DisplayGenerator._findExistingPitchInfo(reading, position, tags, dictionaryResults); + if (pitchInfo === null) { + pitchInfo = {expressions: new Set(), reading, position, tags}; + dictionaryResults.push(pitchInfo); + } + pitchInfo.expressions.add(expression); + } + } + } + + return [...results.entries()]; + } + + static _findExistingPitchInfo(reading, position, tags, pitchInfoList) { + for (const pitchInfo of pitchInfoList) { + if ( + pitchInfo.reading === reading && + pitchInfo.position === position && + DisplayGenerator._areTagListsEqual(pitchInfo.tags, tags) + ) { + return pitchInfo; + } + } + return null; + } + + static _areTagListsEqual(tagList1, tagList2) { + const ii = tagList1.length; + if (tagList2.length !== ii) { return false; } + + for (let i = 0; i < ii; ++i) { + const tag1 = tagList1[i]; + const tag2 = tagList2[i]; + if (tag1.name !== tag2.name || tag1.dictionary !== tag2.dictionary) { + return false; + } + } + + return true; + } + + static _jpGetKanaMorae(text) { + // This function splits Japanese kana reading into its individual mora + // components. It is assumed that the text is well-formed. + const smallKanaSet = DisplayGenerator._smallKanaSet; + const morae = []; + let i; + for (const c of text) { + if (smallKanaSet.has(c) && (i = morae.length) > 0) { + morae[i - 1] += c; + } else { + morae.push(c); + } + } + return morae; + } + + static _jpCreateSmallKanaSet() { + return new Set(Array.from('ぁぃぅぇぉゃゅょゎァィゥェォャュョヮ')); + } + + static _jpIsMoraPitchHigh(moraIndex, pitchAccentPosition) { + return pitchAccentPosition === 0 ? (moraIndex > 0) : (moraIndex < pitchAccentPosition); + } } + +DisplayGenerator._smallKanaSet = DisplayGenerator._jpCreateSmallKanaSet(); |