aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortoasted-nutbread <toasted-nutbread@users.noreply.github.com>2020-04-05 12:51:27 -0400
committerGitHub <noreply@github.com>2020-04-05 12:51:27 -0400
commitf439d12718247411ccd0575af0d1de82aa22564a (patch)
tree2989cc53af496a9d9b84134d7fd761300a8ddee4
parent167e83c14794437e43b7df2017efab1e7e060a99 (diff)
parent938b69646820482a958cd8e0a65a03400ee6a7ac (diff)
Merge pull request #385 from toasted-nutbread/pitch-accents
Pitch accents
-rw-r--r--.eslintrc.json2
-rw-r--r--ext/bg/data/dictionary-term-meta-bank-v3-schema.json64
-rw-r--r--ext/bg/data/options-schema.json17
-rw-r--r--ext/bg/js/dictionary.js32
-rw-r--r--ext/bg/js/options.js5
-rw-r--r--ext/bg/js/settings/main.js6
-rw-r--r--ext/bg/js/translator.js22
-rw-r--r--ext/bg/settings.html12
-rw-r--r--ext/mixed/css/display-dark.css27
-rw-r--r--ext/mixed/css/display-default.css27
-rw-r--r--ext/mixed/css/display.css190
-rw-r--r--ext/mixed/display-templates.html17
-rw-r--r--ext/mixed/js/core.js24
-rw-r--r--ext/mixed/js/display-generator.js246
-rw-r--r--ext/mixed/js/display.js3
-rw-r--r--ext/mixed/js/japanese.js26
-rw-r--r--test/data/dictionaries/valid-dictionary1/tag_bank_3.json4
-rw-r--r--test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json36
-rw-r--r--test/test-database.js9
-rw-r--r--test/test-japanese.js55
20 files changed, 770 insertions, 54 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index 9bd6514a..8882cb42 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -87,6 +87,8 @@
"stringReverse": "readonly",
"promiseTimeout": "readonly",
"parseUrl": "readonly",
+ "areSetsEqual": "readonly",
+ "getSetIntersection": "readonly",
"EventDispatcher": "readonly",
"EventListenerCollection": "readonly",
"EXTENSION_IS_BROWSER_EDGE": "readonly"
diff --git a/ext/bg/data/dictionary-term-meta-bank-v3-schema.json b/ext/bg/data/dictionary-term-meta-bank-v3-schema.json
index 1cc0557f..8475db81 100644
--- a/ext/bg/data/dictionary-term-meta-bank-v3-schema.json
+++ b/ext/bg/data/dictionary-term-meta-bank-v3-schema.json
@@ -13,13 +13,71 @@
},
{
"type": "string",
- "enum": ["freq"],
- "description": "Type of data. \"freq\" corresponds to frequency information."
+ "enum": ["freq", "pitch"],
+ "description": "Type of data. \"freq\" corresponds to frequency information; \"pitch\" corresponds to pitch information."
},
{
- "type": ["string", "number"],
"description": "Data for the term/expression."
}
+ ],
+ "oneOf": [
+ {
+ "items": [
+ {},
+ {"enum": ["freq"]},
+ {
+ "type": ["string", "number"],
+ "description": "Frequency information for the term or expression."
+ }
+ ]
+ },
+ {
+ "items": [
+ {},
+ {"enum": ["pitch"]},
+ {
+ "type": ["object"],
+ "description": "Pitch accent information for the term or expression.",
+ "required": [
+ "reading",
+ "pitches"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "reading": {
+ "type": "string",
+ "description": "Reading for the term or expression."
+ },
+ "pitches": {
+ "type": "array",
+ "description": "List of different pitch accent information for the term and reading combination.",
+ "additionalItems": {
+ "type": "object",
+ "required": [
+ "position"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "position": {
+ "type": "integer",
+ "description": "Mora position of the pitch accent downstep. A value of 0 indicates that the word does not have a downstep (heiban).",
+ "minimum": 0
+ },
+ "tags": {
+ "type": "array",
+ "description": "List of tags for this pitch accent.",
+ "items": {
+ "type": "string",
+ "description": "Tag for this pitch accent. This typically corresponds to a certain type of part of speech."
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
]
}
} \ No newline at end of file
diff --git a/ext/bg/data/options-schema.json b/ext/bg/data/options-schema.json
index d6207952..cb759b72 100644
--- a/ext/bg/data/options-schema.json
+++ b/ext/bg/data/options-schema.json
@@ -105,7 +105,10 @@
"customPopupCss",
"customPopupOuterCss",
"enableWanakana",
- "enableClipboardMonitor"
+ "enableClipboardMonitor",
+ "showPitchAccentDownstepNotation",
+ "showPitchAccentPositionNotation",
+ "showPitchAccentGraph"
],
"properties": {
"enable": {
@@ -227,6 +230,18 @@
"enableClipboardMonitor": {
"type": "boolean",
"default": false
+ },
+ "showPitchAccentDownstepNotation": {
+ "type": "boolean",
+ "default": true
+ },
+ "showPitchAccentPositionNotation": {
+ "type": "boolean",
+ "default": true
+ },
+ "showPitchAccentGraph": {
+ "type": "boolean",
+ "default": false
}
}
},
diff --git a/ext/bg/js/dictionary.js b/ext/bg/js/dictionary.js
index 3dd1d0c1..74bd5a64 100644
--- a/ext/bg/js/dictionary.js
+++ b/ext/bg/js/dictionary.js
@@ -137,30 +137,6 @@ function dictTermsGroup(definitions, dictionaries) {
return dictTermsSort(results);
}
-function dictAreSetsEqual(set1, set2) {
- if (set1.size !== set2.size) {
- return false;
- }
-
- for (const value of set1) {
- if (!set2.has(value)) {
- return false;
- }
- }
-
- return true;
-}
-
-function dictGetSetIntersection(set1, set2) {
- const result = [];
- for (const value of set1) {
- if (set2.has(value)) {
- result.push(value);
- }
- }
- return result;
-}
-
function dictTermsMergeBySequence(definitions, mainDictionary) {
const sequencedDefinitions = new Map();
const nonSequencedDefinitions = [];
@@ -281,11 +257,11 @@ function dictTermsMergeByGloss(result, definitions, appendTo=null, mergedIndices
const only = [];
const expressionSet = definition.expression;
const readingSet = definition.reading;
- if (!dictAreSetsEqual(expressionSet, resultExpressionSet)) {
- only.push(...dictGetSetIntersection(expressionSet, resultExpressionSet));
+ if (!areSetsEqual(expressionSet, resultExpressionSet)) {
+ only.push(...getSetIntersection(expressionSet, resultExpressionSet));
}
- if (!dictAreSetsEqual(readingSet, resultReadingSet)) {
- only.push(...dictGetSetIntersection(readingSet, resultReadingSet));
+ if (!areSetsEqual(readingSet, resultReadingSet)) {
+ only.push(...getSetIntersection(readingSet, resultReadingSet));
}
definition.only = only;
}
diff --git a/ext/bg/js/options.js b/ext/bg/js/options.js
index bd0bbe0e..b36fe812 100644
--- a/ext/bg/js/options.js
+++ b/ext/bg/js/options.js
@@ -124,7 +124,10 @@ function profileOptionsCreateDefaults() {
customPopupCss: '',
customPopupOuterCss: '',
enableWanakana: true,
- enableClipboardMonitor: false
+ enableClipboardMonitor: false,
+ showPitchAccentDownstepNotation: true,
+ showPitchAccentPositionNotation: true,
+ showPitchAccentGraph: false
},
audio: {
diff --git a/ext/bg/js/settings/main.js b/ext/bg/js/settings/main.js
index ebc443df..7caeaea0 100644
--- a/ext/bg/js/settings/main.js
+++ b/ext/bg/js/settings/main.js
@@ -84,6 +84,9 @@ async function formRead(options) {
options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val());
options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked');
options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').prop('checked');
+ options.general.showPitchAccentDownstepNotation = $('#show-pitch-accent-downstep-notation').prop('checked');
+ options.general.showPitchAccentPositionNotation = $('#show-pitch-accent-position-notation').prop('checked');
+ options.general.showPitchAccentGraph = $('#show-pitch-accent-graph').prop('checked');
options.general.popupTheme = $('#popup-theme').val();
options.general.popupOuterTheme = $('#popup-outer-theme').val();
options.general.customPopupCss = $('#custom-popup-css').val();
@@ -161,6 +164,9 @@ async function formWrite(options) {
$('#popup-scaling-factor').val(options.general.popupScalingFactor);
$('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom);
$('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport);
+ $('#show-pitch-accent-downstep-notation').prop('checked', options.general.showPitchAccentDownstepNotation);
+ $('#show-pitch-accent-position-notation').prop('checked', options.general.showPitchAccentPositionNotation);
+ $('#show-pitch-accent-graph').prop('checked', options.general.showPitchAccentGraph);
$('#popup-theme').val(options.general.popupTheme);
$('#popup-outer-theme').val(options.general.popupOuterTheme);
$('#custom-popup-css').val(options.general.customPopupCss);
diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js
index 6f43f7b0..f16889ce 100644
--- a/ext/bg/js/translator.js
+++ b/ext/bg/js/translator.js
@@ -490,6 +490,7 @@ class Translator {
// New data
term.frequencies = [];
+ term.pitches = [];
}
const metas = await this.database.findTermMetaBulk(expressionsUnique, dictionaries);
@@ -500,6 +501,13 @@ class Translator {
term.frequencies.push({expression, frequency: data, dictionary});
}
break;
+ case 'pitch':
+ for (const term of termsUnique[index]) {
+ const pitchData = await this.getPitchData(expression, data, dictionary, term);
+ if (pitchData === null) { continue; }
+ term.pitches.push(pitchData);
+ }
+ break;
}
}
}
@@ -583,6 +591,20 @@ class Translator {
return tagMetaList;
}
+ async getPitchData(expression, data, dictionary, term) {
+ const reading = data.reading;
+ const termReading = term.reading || expression;
+ if (reading !== termReading) { return null; }
+
+ const pitches = [];
+ for (let {position, tags} of data.pitches) {
+ tags = Array.isArray(tags) ? await this.getTagMetaList(tags, dictionary) : [];
+ pitches.push({position, tags});
+ }
+
+ return {reading, pitches, dictionary};
+ }
+
static createExpression(expression, reading, termTags=null, termFrequency=null) {
const furiganaSegments = jp.distributeFurigana(expression, reading);
return {
diff --git a/ext/bg/settings.html b/ext/bg/settings.html
index cfe20be4..0b2e4f9c 100644
--- a/ext/bg/settings.html
+++ b/ext/bg/settings.html
@@ -163,6 +163,18 @@
</div>
<div class="checkbox options-advanced">
+ <label><input type="checkbox" id="show-pitch-accent-downstep-notation"> Show downstep notation for pitch accents</label>
+ </div>
+
+ <div class="checkbox options-position">
+ <label><input type="checkbox" id="show-pitch-accent-position-notation"> Show position notation for pitch accents</label>
+ </div>
+
+ <div class="checkbox options-advanced">
+ <label><input type="checkbox" id="show-pitch-accent-graph"> Show graph for pitch accents</label>
+ </div>
+
+ <div class="checkbox options-advanced">
<label><input type="checkbox" id="show-debug-info"> Show debug information</label>
</div>
diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css
index c9cd9f90..550dff3e 100644
--- a/ext/mixed/css/display-dark.css
+++ b/ext/mixed/css/display-dark.css
@@ -19,6 +19,8 @@
body { background-color: #1e1e1e; color: #d4d4d4; }
+h2 { border-bottom-color: #2f2f2f; }
+
.navigation-header {
background-color: #1e1e1e;
border-bottom-color: #2f2f2f;
@@ -39,6 +41,7 @@ body { background-color: #1e1e1e; color: #d4d4d4; }
.tag[data-category=frequency] { background-color: #489148; }
.tag[data-category=partOfSpeech] { background-color: #565656; }
.tag[data-category=search] { background-color: #69696e; }
+.tag[data-category=pitch-accent-dictionary] { background-color: #6640be; }
.term-reasons { color: #888888; }
@@ -57,12 +60,15 @@ body { background-color: #1e1e1e; color: #d4d4d4; }
color: #666666;
}
-.term-definition-container,
-.kanji-glossary-container {
+.term-definition-list,
+.term-pitch-accent-group-list,
+.term-pitch-accent-disambiguation-list,
+.kanji-glossary-list {
color: #888888;
}
.term-glossary,
+.term-pitch-accent,
.kanji-glossary {
color: #d4d4d4;
}
@@ -72,3 +78,20 @@ body { background-color: #1e1e1e; color: #d4d4d4; }
background-color: #d4d4d4;
color: #1e1e1e;
}
+
+.term-pitch-accent-container { border-bottom-color: #2f2f2f; }
+
+.term-pitch-accent-character:before { border-color: #ffffff; }
+
+.term-pitch-accent-graph-line,
+.term-pitch-accent-graph-line-tail,
+#term-pitch-accent-graph-dot,
+#term-pitch-accent-graph-dot-downstep,
+#term-pitch-accent-graph-triangle {
+ stroke: #ffffff;
+}
+
+#term-pitch-accent-graph-dot,
+#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
+ fill: #ffffff;
+}
diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css
index 6eee43c4..487b8cb8 100644
--- a/ext/mixed/css/display-default.css
+++ b/ext/mixed/css/display-default.css
@@ -19,6 +19,8 @@
body { background-color: #ffffff; color: #333333; }
+h2 { border-bottom-color: #eeeeee; }
+
.navigation-header {
background-color: #ffffff;
border-bottom-color: #eeeeee;
@@ -39,6 +41,7 @@ body { background-color: #ffffff; color: #333333; }
.tag[data-category=frequency] { background-color: #5cb85c; }
.tag[data-category=partOfSpeech] { background-color: #565656; }
.tag[data-category=search] { background-color: #8a8a91; }
+.tag[data-category=pitch-accent-dictionary] { background-color: #6640be; }
.term-reasons { color: #777777; }
@@ -57,12 +60,15 @@ body { background-color: #ffffff; color: #333333; }
color: #999999;
}
-.term-definition-container,
-.kanji-glossary-container {
+.term-definition-list,
+.term-pitch-accent-group-list,
+.term-pitch-accent-disambiguation-list,
+.kanji-glossary-list {
color: #777777;
}
.term-glossary,
+.term-pitch-accent,
.kanji-glossary {
color: #000000;
}
@@ -72,3 +78,20 @@ body { background-color: #ffffff; color: #333333; }
background-color: #333333;
color: #ffffff;
}
+
+.term-pitch-accent-container { border-bottom-color: #eeeeee; }
+
+.term-pitch-accent-character:before { border-color: #000000; }
+
+.term-pitch-accent-graph-line,
+.term-pitch-accent-graph-line-tail,
+#term-pitch-accent-graph-dot,
+#term-pitch-accent-graph-dot-downstep,
+#term-pitch-accent-graph-triangle {
+ stroke: #000000;
+}
+
+#term-pitch-accent-graph-dot,
+#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
+ fill: #000000;
+}
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index 688a357c..a4432016 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -65,6 +65,14 @@ ol, ul {
height: 2.28571428em; /* 14px => 32px */
}
+h2 {
+ font-size: 1.25em;
+ font-weight: normal;
+ margin: 0.25em 0 0;
+ border-bottom-width: 0.05714285714285714em; /* 14px * 1.25em => 1px */
+ border-bottom-style: solid;
+}
+
/*
* Navigation
*/
@@ -302,6 +310,7 @@ button.action-button {
width: 0;
height: 0;
visibility: hidden;
+ z-index: 1;
}
.term-expression-list[data-multi=true] .term-expression:hover .term-expression-details {
@@ -422,6 +431,187 @@ button.action-button {
display: inline;
}
+.term-entry-body[data-section-count="0"] .term-entry-body-section-header,
+.term-entry-body[data-section-count="1"] .term-entry-body-section-header {
+ display: none;
+}
+
+
+/*
+ * Pitch accent styles
+ */
+
+.entry[data-pitch-accent-count='0'] .term-pitch-accent-container {
+ display: none;
+}
+
+.term-pitch-accent-container {
+ border-bottom-width: 0.05714285714285714em; /* 14px * 1.25em => 1px */
+ border-bottom-style: solid;
+ padding-bottom: 0.25em;
+ margin-bottom: 0.25em;
+}
+
+.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;
+}
+
+.term-pitch-accent-disambiguation-list {
+ padding-right: 0.25em;
+}
+
+.term-pitch-accent-disambiguation-list:before {
+ content: "(";
+}
+
+.term-pitch-accent-disambiguation-list:after {
+ content: " only)";
+}
+
+.term-pitch-accent-disambiguation+.term-pitch-accent-disambiguation:before {
+ content: ", ";
+}
+
+.term-pitch-accent-disambiguation-list[data-count="0"],
+:root[data-show-pitch-accent-downstep-notation=true] .term-pitch-accent-disambiguation-list[data-expression-count="0"],
+:root[data-show-pitch-accent-downstep-notation=true] .term-pitch-accent-disambiguation[data-type=reading] {
+ display: none;
+}
+
+.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;
+}
+
+
+:root[data-show-pitch-accent-downstep-notation=false] .term-pitch-accent-characters {
+ display: none;
+}
+
+:root[data-show-pitch-accent-position-notation=false] .term-pitch-accent-position {
+ display: none;
+}
+
+:root[data-show-pitch-accent-graph=false] .term-pitch-accent-details {
+ display: none;
+}
+
+
+/*
+ * Pitch accent graph styles
+ */
+
+.term-pitch-accent-graph {
+ display: block;
+ height: 1.5em;
+ transform: translateY(-0.875em);
+}
+.term-pitch-accent-graph-line,
+.term-pitch-accent-graph-line-tail {
+ fill: none;
+ stroke: #000000;
+ stroke-width: 5;
+}
+.term-pitch-accent-graph-line-tail {
+ stroke-dasharray: 5 5;
+}
+#term-pitch-accent-graph-dot {
+ fill: #000000;
+ stroke: #000000;
+ stroke-width: 5;
+}
+#term-pitch-accent-graph-dot-downstep {
+ fill: none;
+ stroke: #000000;
+ stroke-width: 5;
+}
+#term-pitch-accent-graph-dot-downstep>circle:last-of-type {
+ fill: #000000;
+}
+#term-pitch-accent-graph-triangle {
+ fill: none;
+ stroke: #000000;
+ stroke-width: 5;
+}
+
/*
* Kanji
diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html
index 7ae51a62..b8d52d15 100644
--- a/ext/mixed/display-templates.html
+++ b/ext/mixed/display-templates.html
@@ -17,7 +17,10 @@
</div>
<div class="term-special-tags"><div class="frequencies tag-list"></div></div>
</div>
- <div class="term-definition-container"><ol class="term-definition-list"></ol></div>
+ <div class="term-entry-body">
+ <div class="term-entry-body-section term-pitch-accent-container"><ol class="term-entry-body-section-content term-pitch-accent-group-list"></ol></div>
+ <div class="term-entry-body-section term-definition-container"><ol class="term-entry-body-section-content term-definition-list"></ol></div>
+ </div>
<pre class="debug-info"></pre>
</div></template>
<template id="term-expression-template"><div class="term-expression"><span class="term-expression-text source-text"></span><div class="term-expression-details">
@@ -34,6 +37,18 @@
<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-static-template"><svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
+ <defs>
+ <g id="term-pitch-accent-graph-dot"><circle cx="0" cy="0" r="15" /></g>
+ <g id="term-pitch-accent-graph-dot-downstep"><circle cx="0" cy="0" r="15" /><circle cx="0" cy="0" r="5" /></g>
+ <g id="term-pitch-accent-graph-triangle"><path d="M0 13 L15 -13 L-15 -13 Z" /></g>
+ </defs>
+</svg></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-disambiguation-template"><span class="term-pitch-accent-disambiguation"></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-disambiguation-list"></span><span class="term-pitch-accent-characters"></span><span class="term-pitch-accent-position"></span><span class="term-pitch-accent-details"><svg class="term-pitch-accent-graph" xmlns="http://www.w3.org/2000/svg"><path class="term-pitch-accent-graph-line" /><path class="term-pitch-accent-graph-line-tail" /></svg></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/core.js b/ext/mixed/js/core.js
index 0d50e915..fd762e97 100644
--- a/ext/mixed/js/core.js
+++ b/ext/mixed/js/core.js
@@ -132,6 +132,30 @@ function parseUrl(url) {
return {baseUrl, queryParams};
}
+function areSetsEqual(set1, set2) {
+ if (set1.size !== set2.size) {
+ return false;
+ }
+
+ for (const value of set1) {
+ if (!set2.has(value)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function getSetIntersection(set1, set2) {
+ const result = [];
+ for (const value of set1) {
+ if (set2.has(value)) {
+ result.push(value);
+ }
+ }
+ return result;
+}
+
/*
* Async utilities
diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js
index 41f7315a..f1122e3d 100644
--- a/ext/mixed/js/display-generator.js
+++ b/ext/mixed/js/display-generator.js
@@ -25,6 +25,7 @@
class DisplayGenerator {
constructor() {
this._templateHandler = null;
+ this._termPitchAccentStaticTemplateIsSetup = false;
}
async prepare() {
@@ -37,17 +38,33 @@ 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);
+ const expressionCount = expressionMulti ? details.expressions.length : 1;
+ const definitionCount = definitionMulti ? details.definitions.length : 1;
+ const uniqueExpressionCount = Array.isArray(details.expression) ? new Set(details.expression).size : 1;
node.dataset.expressionMulti = `${expressionMulti}`;
node.dataset.definitionMulti = `${definitionMulti}`;
- node.dataset.expressionCount = `${expressionMulti ? details.expressions.length : 1}`;
- node.dataset.definitionCount = `${definitionMulti ? details.definitions.length : 1}`;
+ 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) +
+ (pitches.length > 0 ? 1 : 0)
+ }`;
const termTags = details.termTags;
let expressions = details.expressions;
@@ -56,6 +73,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) {
@@ -262,6 +280,133 @@ 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: 'pitch-accent-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 {reading, position, tags, exclusiveExpressions, exclusiveReadings} = details;
+ const morae = jp.getKanaMorae(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-disambiguation-list');
+ this.createPitchAccentDisambiguations(n, exclusiveExpressions, exclusiveReadings);
+
+ n = node.querySelector('.term-pitch-accent-characters');
+ for (let i = 0, ii = morae.length; i < ii; ++i) {
+ const mora = morae[i];
+ const highPitch = jp.isMoraPitchHigh(i, position);
+ const highPitchNext = jp.isMoraPitchHigh(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);
+ }
+
+ if (morae.length > 0) {
+ this.populatePitchGraph(node.querySelector('.term-pitch-accent-graph'), position, morae);
+ }
+
+ return node;
+ }
+
+ createPitchAccentDisambiguations(container, exclusiveExpressions, exclusiveReadings) {
+ const templateName = 'term-pitch-accent-disambiguation';
+ for (const exclusiveExpression of exclusiveExpressions) {
+ const node = this._templateHandler.instantiate(templateName);
+ node.dataset.type = 'expression';
+ node.textContent = exclusiveExpression;
+ container.appendChild(node);
+ }
+
+ for (const exclusiveReading of exclusiveReadings) {
+ const node = this._templateHandler.instantiate(templateName);
+ node.dataset.type = 'reading';
+ node.textContent = exclusiveReading;
+ container.appendChild(node);
+ }
+
+ container.dataset.multi = 'true';
+ container.dataset.count = `${exclusiveExpressions.length + exclusiveReadings.length}`;
+ container.dataset.expressionCount = `${exclusiveExpressions.length}`;
+ container.dataset.readingCount = `${exclusiveReadings.length}`;
+ }
+
+ populatePitchGraph(svg, position, morae) {
+ const svgns = svg.getAttribute('xmlns');
+ const ii = morae.length;
+ svg.setAttribute('viewBox', `0 0 ${50 * (ii + 1)} 100`);
+
+ const pathPoints = [];
+ for (let i = 0; i < ii; ++i) {
+ const highPitch = jp.isMoraPitchHigh(i, position);
+ const highPitchNext = jp.isMoraPitchHigh(i + 1, position);
+ const graphic = (highPitch && !highPitchNext ? '#term-pitch-accent-graph-dot-downstep' : '#term-pitch-accent-graph-dot');
+ const x = `${i * 50 + 25}`;
+ const y = highPitch ? '25' : '75';
+ const use = document.createElementNS(svgns, 'use');
+ use.setAttribute('href', graphic);
+ use.setAttribute('x', x);
+ use.setAttribute('y', y);
+ svg.appendChild(use);
+ pathPoints.push(`${x} ${y}`);
+ }
+
+ let path = svg.querySelector('.term-pitch-accent-graph-line');
+ path.setAttribute('d', `M${pathPoints.join(' L')}`);
+
+ pathPoints.splice(0, ii - 1);
+ {
+ const highPitch = jp.isMoraPitchHigh(ii, position);
+ const x = `${ii * 50 + 25}`;
+ const y = highPitch ? '25' : '75';
+ const use = document.createElementNS(svgns, 'use');
+ use.setAttribute('href', '#term-pitch-accent-graph-triangle');
+ use.setAttribute('x', x);
+ use.setAttribute('y', y);
+ svg.appendChild(use);
+ pathPoints.push(`${x} ${y}`);
+ }
+
+ path = svg.querySelector('.term-pitch-accent-graph-line-tail');
+ path.setAttribute('d', `M${pathPoints.join(' L')}`);
+ }
+
createFrequencyTag(details) {
const node = this._templateHandler.instantiate('tag-frequency');
@@ -301,22 +446,28 @@ class DisplayGenerator {
}
}
- static _appendMultiple(container, createItem, detailsArray, fallback=[]) {
+ static _appendMultiple(container, createItem, detailsIterable, fallback=[]) {
if (container === null) { return 0; }
- const isArray = Array.isArray(detailsArray);
- if (!isArray) { detailsArray = fallback; }
-
- container.dataset.multi = `${isArray}`;
- container.dataset.count = `${detailsArray.length}`;
+ const multi = (
+ detailsIterable !== null &&
+ typeof detailsIterable === 'object' &&
+ typeof detailsIterable[Symbol.iterator] !== 'undefined'
+ );
+ if (!multi) { detailsIterable = fallback; }
- for (const details of detailsArray) {
+ let count = 0;
+ for (const details of detailsIterable) {
const item = createItem(details);
if (item === null) { continue; }
container.appendChild(item);
+ ++count;
}
- return detailsArray.length;
+ container.dataset.multi = `${multi}`;
+ container.dataset.count = `${count}`;
+
+ return count;
}
static _appendFurigana(container, segments, addText) {
@@ -342,4 +493,79 @@ class DisplayGenerator {
container.appendChild(document.createTextNode(parts[i]));
}
}
+
+ static _getPitchInfos(definition) {
+ const results = new Map();
+
+ const allExpressions = new Set();
+ const allReadings = new Set();
+ const expressions = definition.expressions;
+ const sources = Array.isArray(expressions) ? expressions : [definition];
+ for (const {pitches: expressionPitches, expression} of sources) {
+ allExpressions.add(expression);
+ for (const {reading, pitches, dictionary} of expressionPitches) {
+ allReadings.add(reading);
+ 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);
+ }
+ }
+ }
+
+ for (const dictionaryResults of results.values()) {
+ for (const result of dictionaryResults) {
+ const exclusiveExpressions = [];
+ const exclusiveReadings = [];
+ const resultExpressions = result.expressions;
+ if (!areSetsEqual(resultExpressions, allExpressions)) {
+ exclusiveExpressions.push(...getSetIntersection(resultExpressions, allExpressions));
+ }
+ if (allReadings.size > 1) {
+ exclusiveReadings.push(result.reading);
+ }
+ result.exclusiveExpressions = exclusiveExpressions;
+ result.exclusiveReadings = exclusiveReadings;
+ }
+ }
+
+ 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;
+ }
}
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index 6898a6eb..4a71efe0 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -385,6 +385,9 @@ class Display {
data.audioEnabled = `${options.audio.enabled}`;
data.compactGlossaries = `${options.general.compactGlossaries}`;
data.enableSearchTags = `${options.scanning.enableSearchTags}`;
+ data.showPitchAccentDownstepNotation = `${options.general.showPitchAccentDownstepNotation}`;
+ data.showPitchAccentPositionNotation = `${options.general.showPitchAccentPositionNotation}`;
+ data.showPitchAccentGraph = `${options.general.showPitchAccentGraph}`;
data.debug = `${options.general.debugInfo}`;
}
diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js
index 61a247b2..e6b9a8a0 100644
--- a/ext/mixed/js/japanese.js
+++ b/ext/mixed/js/japanese.js
@@ -64,6 +64,8 @@ const jp = (() => {
[0xffe0, 0xffee] // Currency markers
];
+ const SMALL_KANA_SET = new Set(Array.from('ぁぃぅぇぉゃゅょゎァィゥェォャュョヮ'));
+
// Character code testing functions
@@ -112,6 +114,26 @@ const jp = (() => {
}
+ // Mora functions
+
+ function isMoraPitchHigh(moraIndex, pitchAccentPosition) {
+ return pitchAccentPosition === 0 ? (moraIndex > 0) : (moraIndex < pitchAccentPosition);
+ }
+
+ function getKanaMorae(text) {
+ const morae = [];
+ let i;
+ for (const c of text) {
+ if (SMALL_KANA_SET.has(c) && (i = morae.length) > 0) {
+ morae[i - 1] += c;
+ } else {
+ morae.push(c);
+ }
+ }
+ return morae;
+ }
+
+
// Exports
return {
@@ -119,6 +141,8 @@ const jp = (() => {
isCodePointKana,
isCodePointJapanese,
isStringEntirelyKana,
- isStringPartiallyJapanese
+ isStringPartiallyJapanese,
+ isMoraPitchHigh,
+ getKanaMorae
};
})();
diff --git a/test/data/dictionaries/valid-dictionary1/tag_bank_3.json b/test/data/dictionaries/valid-dictionary1/tag_bank_3.json
new file mode 100644
index 00000000..572221fe
--- /dev/null
+++ b/test/data/dictionaries/valid-dictionary1/tag_bank_3.json
@@ -0,0 +1,4 @@
+[
+ ["ptag1", "pcategory1", 0, "ptag1 notes", 0],
+ ["ptag2", "pcategory2", 0, "ptag2 notes", 0]
+] \ No newline at end of file
diff --git a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
index 78096502..26922394 100644
--- a/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
+++ b/test/data/dictionaries/valid-dictionary1/term_meta_bank_1.json
@@ -1,5 +1,39 @@
[
["打", "freq", 1],
["打つ", "freq", 2],
- ["打ち込む", "freq", 3]
+ ["打ち込む", "freq", 3],
+ [
+ "打ち込む",
+ "pitch",
+ {
+ "reading": "うちこむ",
+ "pitches": [
+ {"position": 0},
+ {"position": 3}
+ ]
+ }
+ ],
+ [
+ "打ち込む",
+ "pitch",
+ {
+ "reading": "ぶちこむ",
+ "pitches": [
+ {"position": 0},
+ {"position": 3}
+ ]
+ }
+ ],
+ [
+ "お手前",
+ "pitch",
+ {
+ "reading": "おてまえ",
+ "pitches": [
+ {"position": 2, "tags": ["ptag1"]},
+ {"position": 2, "tags": ["ptag2"]},
+ {"position": 0, "tags": ["ptag2"]}
+ ]
+ }
+ ]
] \ No newline at end of file
diff --git a/test/test-database.js b/test/test-database.js
index 833aa75d..dbd67257 100644
--- a/test/test-database.js
+++ b/test/test-database.js
@@ -231,8 +231,8 @@ async function testDatabase1() {
true
);
vm.assert.deepStrictEqual(counts, {
- counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}],
- total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 3, tagMeta: 12}
+ counts: [{kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14}],
+ total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14}
});
// Test find* functions
@@ -648,9 +648,10 @@ async function testFindTermMetaBulk1(database, titles) {
}
],
expectedResults: {
- total: 1,
+ total: 3,
modes: [
- ['freq', 1]
+ ['freq', 1],
+ ['pitch', 2]
]
}
},
diff --git a/test/test-japanese.js b/test/test-japanese.js
index c5d220e7..eab632bf 100644
--- a/test/test-japanese.js
+++ b/test/test-japanese.js
@@ -392,6 +392,59 @@ function testDistributeFuriganaInflected() {
}
}
+function testIsMoraPitchHigh() {
+ const data = [
+ [[0, 0], false],
+ [[1, 0], true],
+ [[2, 0], true],
+ [[3, 0], true],
+
+ [[0, 1], true],
+ [[1, 1], false],
+ [[2, 1], false],
+ [[3, 1], false],
+
+ [[0, 2], true],
+ [[1, 2], true],
+ [[2, 2], false],
+ [[3, 2], false],
+
+ [[0, 3], true],
+ [[1, 3], true],
+ [[2, 3], true],
+ [[3, 3], false],
+
+ [[0, 4], true],
+ [[1, 4], true],
+ [[2, 4], true],
+ [[3, 4], true]
+ ];
+
+ for (const [[moraIndex, pitchAccentPosition], expected] of data) {
+ const actual = jp.isMoraPitchHigh(moraIndex, pitchAccentPosition);
+ assert.strictEqual(actual, expected);
+ }
+}
+
+function testGetKanaMorae() {
+ const data = [
+ ['かこ', ['か', 'こ']],
+ ['かっこ', ['か', 'っ', 'こ']],
+ ['カコ', ['カ', 'コ']],
+ ['カッコ', ['カ', 'ッ', 'コ']],
+ ['コート', ['コ', 'ー', 'ト']],
+ ['ちゃんと', ['ちゃ', 'ん', 'と']],
+ ['とうきょう', ['と', 'う', 'きょ', 'う']],
+ ['ぎゅう', ['ぎゅ', 'う']],
+ ['ディスコ', ['ディ', 'ス', 'コ']]
+ ];
+
+ for (const [text, expected] of data) {
+ const actual = jp.getKanaMorae(text);
+ vm.assert.deepStrictEqual(actual, expected);
+ }
+}
+
function main() {
testIsCodePointKanji();
@@ -408,6 +461,8 @@ function main() {
testConvertAlphabeticToKana();
testDistributeFurigana();
testDistributeFuriganaInflected();
+ testIsMoraPitchHigh();
+ testGetKanaMorae();
}