aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-03-01 14:15:28 -0500
committertoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-03-28 10:25:57 -0400
commit97a520cc1595369dc18ddcf74ab7f0ba4e03f55b (patch)
tree9ee58cba18c0e4f60571ab6266a91b3458633a3e
parent2d7214ce60ef55b0d3d6e98b86f41b4e9fce5c48 (diff)
Add support for displaying pitch accents
-rw-r--r--ext/mixed/css/display-dark.css8
-rw-r--r--ext/mixed/css/display-default.css8
-rw-r--r--ext/mixed/css/display.css101
-rw-r--r--ext/mixed/display-templates.html6
-rw-r--r--ext/mixed/js/display-generator.js159
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();