From 97a520cc1595369dc18ddcf74ab7f0ba4e03f55b Mon Sep 17 00:00:00 2001
From: toasted-nutbread <toasted-nutbread@users.noreply.github.com>
Date: Sun, 1 Mar 2020 14:15:28 -0500
Subject: Add support for displaying pitch accents

---
 ext/mixed/css/display-dark.css    |   8 +-
 ext/mixed/css/display-default.css |   8 +-
 ext/mixed/css/display.css         | 101 ++++++++++++++++++++++++
 ext/mixed/display-templates.html  |   6 ++
 ext/mixed/js/display-generator.js | 159 +++++++++++++++++++++++++++++++++++++-
 5 files changed, 277 insertions(+), 5 deletions(-)

(limited to 'ext/mixed')

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
@@ -436,6 +436,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();
-- 
cgit v1.2.3