Merge pull request #385 from toasted-nutbread/pitch-accents

Pitch accents
This commit is contained in:
toasted-nutbread 2020-04-05 12:51:27 -04:00 committed by GitHub
commit f439d12718
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 770 additions and 54 deletions

View File

@ -87,6 +87,8 @@
"stringReverse": "readonly", "stringReverse": "readonly",
"promiseTimeout": "readonly", "promiseTimeout": "readonly",
"parseUrl": "readonly", "parseUrl": "readonly",
"areSetsEqual": "readonly",
"getSetIntersection": "readonly",
"EventDispatcher": "readonly", "EventDispatcher": "readonly",
"EventListenerCollection": "readonly", "EventListenerCollection": "readonly",
"EXTENSION_IS_BROWSER_EDGE": "readonly" "EXTENSION_IS_BROWSER_EDGE": "readonly"

View File

@ -13,13 +13,71 @@
}, },
{ {
"type": "string", "type": "string",
"enum": ["freq"], "enum": ["freq", "pitch"],
"description": "Type of data. \"freq\" corresponds to frequency information." "description": "Type of data. \"freq\" corresponds to frequency information; \"pitch\" corresponds to pitch information."
}, },
{ {
"type": ["string", "number"],
"description": "Data for the term/expression." "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."
}
}
}
}
}
}
}
]
}
] ]
} }
} }

View File

@ -105,7 +105,10 @@
"customPopupCss", "customPopupCss",
"customPopupOuterCss", "customPopupOuterCss",
"enableWanakana", "enableWanakana",
"enableClipboardMonitor" "enableClipboardMonitor",
"showPitchAccentDownstepNotation",
"showPitchAccentPositionNotation",
"showPitchAccentGraph"
], ],
"properties": { "properties": {
"enable": { "enable": {
@ -227,6 +230,18 @@
"enableClipboardMonitor": { "enableClipboardMonitor": {
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"showPitchAccentDownstepNotation": {
"type": "boolean",
"default": true
},
"showPitchAccentPositionNotation": {
"type": "boolean",
"default": true
},
"showPitchAccentGraph": {
"type": "boolean",
"default": false
} }
} }
}, },

View File

@ -137,30 +137,6 @@ function dictTermsGroup(definitions, dictionaries) {
return dictTermsSort(results); 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) { function dictTermsMergeBySequence(definitions, mainDictionary) {
const sequencedDefinitions = new Map(); const sequencedDefinitions = new Map();
const nonSequencedDefinitions = []; const nonSequencedDefinitions = [];
@ -281,11 +257,11 @@ function dictTermsMergeByGloss(result, definitions, appendTo=null, mergedIndices
const only = []; const only = [];
const expressionSet = definition.expression; const expressionSet = definition.expression;
const readingSet = definition.reading; const readingSet = definition.reading;
if (!dictAreSetsEqual(expressionSet, resultExpressionSet)) { if (!areSetsEqual(expressionSet, resultExpressionSet)) {
only.push(...dictGetSetIntersection(expressionSet, resultExpressionSet)); only.push(...getSetIntersection(expressionSet, resultExpressionSet));
} }
if (!dictAreSetsEqual(readingSet, resultReadingSet)) { if (!areSetsEqual(readingSet, resultReadingSet)) {
only.push(...dictGetSetIntersection(readingSet, resultReadingSet)); only.push(...getSetIntersection(readingSet, resultReadingSet));
} }
definition.only = only; definition.only = only;
} }

View File

@ -124,7 +124,10 @@ function profileOptionsCreateDefaults() {
customPopupCss: '', customPopupCss: '',
customPopupOuterCss: '', customPopupOuterCss: '',
enableWanakana: true, enableWanakana: true,
enableClipboardMonitor: false enableClipboardMonitor: false,
showPitchAccentDownstepNotation: true,
showPitchAccentPositionNotation: true,
showPitchAccentGraph: false
}, },
audio: { audio: {

View File

@ -84,6 +84,9 @@ async function formRead(options) {
options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val()); options.general.popupScalingFactor = parseFloat($('#popup-scaling-factor').val());
options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked'); options.general.popupScaleRelativeToPageZoom = $('#popup-scale-relative-to-page-zoom').prop('checked');
options.general.popupScaleRelativeToVisualViewport = $('#popup-scale-relative-to-visual-viewport').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.popupTheme = $('#popup-theme').val();
options.general.popupOuterTheme = $('#popup-outer-theme').val(); options.general.popupOuterTheme = $('#popup-outer-theme').val();
options.general.customPopupCss = $('#custom-popup-css').val(); options.general.customPopupCss = $('#custom-popup-css').val();
@ -161,6 +164,9 @@ async function formWrite(options) {
$('#popup-scaling-factor').val(options.general.popupScalingFactor); $('#popup-scaling-factor').val(options.general.popupScalingFactor);
$('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom); $('#popup-scale-relative-to-page-zoom').prop('checked', options.general.popupScaleRelativeToPageZoom);
$('#popup-scale-relative-to-visual-viewport').prop('checked', options.general.popupScaleRelativeToVisualViewport); $('#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-theme').val(options.general.popupTheme);
$('#popup-outer-theme').val(options.general.popupOuterTheme); $('#popup-outer-theme').val(options.general.popupOuterTheme);
$('#custom-popup-css').val(options.general.customPopupCss); $('#custom-popup-css').val(options.general.customPopupCss);

View File

@ -490,6 +490,7 @@ class Translator {
// New data // New data
term.frequencies = []; term.frequencies = [];
term.pitches = [];
} }
const metas = await this.database.findTermMetaBulk(expressionsUnique, dictionaries); const metas = await this.database.findTermMetaBulk(expressionsUnique, dictionaries);
@ -500,6 +501,13 @@ class Translator {
term.frequencies.push({expression, frequency: data, dictionary}); term.frequencies.push({expression, frequency: data, dictionary});
} }
break; 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; 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) { static createExpression(expression, reading, termTags=null, termFrequency=null) {
const furiganaSegments = jp.distributeFurigana(expression, reading); const furiganaSegments = jp.distributeFurigana(expression, reading);
return { return {

View File

@ -162,6 +162,18 @@
<label><input type="checkbox" id="popup-scale-relative-to-visual-viewport"> Change popup size relative to page viewport</label> <label><input type="checkbox" id="popup-scale-relative-to-visual-viewport"> Change popup size relative to page viewport</label>
</div> </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"> <div class="checkbox options-advanced">
<label><input type="checkbox" id="show-debug-info"> Show debug information</label> <label><input type="checkbox" id="show-debug-info"> Show debug information</label>
</div> </div>

View File

@ -19,6 +19,8 @@
body { background-color: #1e1e1e; color: #d4d4d4; } body { background-color: #1e1e1e; color: #d4d4d4; }
h2 { border-bottom-color: #2f2f2f; }
.navigation-header { .navigation-header {
background-color: #1e1e1e; background-color: #1e1e1e;
border-bottom-color: #2f2f2f; border-bottom-color: #2f2f2f;
@ -39,6 +41,7 @@ body { background-color: #1e1e1e; color: #d4d4d4; }
.tag[data-category=frequency] { background-color: #489148; } .tag[data-category=frequency] { background-color: #489148; }
.tag[data-category=partOfSpeech] { background-color: #565656; } .tag[data-category=partOfSpeech] { background-color: #565656; }
.tag[data-category=search] { background-color: #69696e; } .tag[data-category=search] { background-color: #69696e; }
.tag[data-category=pitch-accent-dictionary] { background-color: #6640be; }
.term-reasons { color: #888888; } .term-reasons { color: #888888; }
@ -57,12 +60,15 @@ body { background-color: #1e1e1e; color: #d4d4d4; }
color: #666666; color: #666666;
} }
.term-definition-container, .term-definition-list,
.kanji-glossary-container { .term-pitch-accent-group-list,
.term-pitch-accent-disambiguation-list,
.kanji-glossary-list {
color: #888888; color: #888888;
} }
.term-glossary, .term-glossary,
.term-pitch-accent,
.kanji-glossary { .kanji-glossary {
color: #d4d4d4; color: #d4d4d4;
} }
@ -72,3 +78,20 @@ body { background-color: #1e1e1e; color: #d4d4d4; }
background-color: #d4d4d4; background-color: #d4d4d4;
color: #1e1e1e; 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;
}

View File

@ -19,6 +19,8 @@
body { background-color: #ffffff; color: #333333; } body { background-color: #ffffff; color: #333333; }
h2 { border-bottom-color: #eeeeee; }
.navigation-header { .navigation-header {
background-color: #ffffff; background-color: #ffffff;
border-bottom-color: #eeeeee; border-bottom-color: #eeeeee;
@ -39,6 +41,7 @@ body { background-color: #ffffff; color: #333333; }
.tag[data-category=frequency] { background-color: #5cb85c; } .tag[data-category=frequency] { background-color: #5cb85c; }
.tag[data-category=partOfSpeech] { background-color: #565656; } .tag[data-category=partOfSpeech] { background-color: #565656; }
.tag[data-category=search] { background-color: #8a8a91; } .tag[data-category=search] { background-color: #8a8a91; }
.tag[data-category=pitch-accent-dictionary] { background-color: #6640be; }
.term-reasons { color: #777777; } .term-reasons { color: #777777; }
@ -57,12 +60,15 @@ body { background-color: #ffffff; color: #333333; }
color: #999999; color: #999999;
} }
.term-definition-container, .term-definition-list,
.kanji-glossary-container { .term-pitch-accent-group-list,
.term-pitch-accent-disambiguation-list,
.kanji-glossary-list {
color: #777777; color: #777777;
} }
.term-glossary, .term-glossary,
.term-pitch-accent,
.kanji-glossary { .kanji-glossary {
color: #000000; color: #000000;
} }
@ -72,3 +78,20 @@ body { background-color: #ffffff; color: #333333; }
background-color: #333333; background-color: #333333;
color: #ffffff; 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;
}

View File

@ -65,6 +65,14 @@ ol, ul {
height: 2.28571428em; /* 14px => 32px */ 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 * Navigation
*/ */
@ -302,6 +310,7 @@ button.action-button {
width: 0; width: 0;
height: 0; height: 0;
visibility: hidden; visibility: hidden;
z-index: 1;
} }
.term-expression-list[data-multi=true] .term-expression:hover .term-expression-details { .term-expression-list[data-multi=true] .term-expression:hover .term-expression-details {
@ -422,6 +431,187 @@ button.action-button {
display: inline; 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 * Kanji

View File

@ -17,7 +17,10 @@
</div> </div>
<div class="term-special-tags"><div class="frequencies tag-list"></div></div> <div class="term-special-tags"><div class="frequencies tag-list"></div></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> <pre class="debug-info"></pre>
</div></template> </div></template>
<template id="term-expression-template"><div class="term-expression"><span class="term-expression-text source-text"></span><div class="term-expression-details"> <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-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-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"> <template id="kanji-entry-template"><div class="entry" data-type="kanji">
<div class="entry-header1"> <div class="entry-header1">
<div class="entry-header2"> <div class="entry-header2">

View File

@ -132,6 +132,30 @@ function parseUrl(url) {
return {baseUrl, queryParams}; 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 * Async utilities

View File

@ -25,6 +25,7 @@
class DisplayGenerator { class DisplayGenerator {
constructor() { constructor() {
this._templateHandler = null; this._templateHandler = null;
this._termPitchAccentStaticTemplateIsSetup = false;
} }
async prepare() { async prepare() {
@ -37,17 +38,33 @@ class DisplayGenerator {
const expressionsContainer = node.querySelector('.term-expression-list'); const expressionsContainer = node.querySelector('.term-expression-list');
const reasonsContainer = node.querySelector('.term-reasons'); const reasonsContainer = node.querySelector('.term-reasons');
const pitchesContainer = node.querySelector('.term-pitch-accent-group-list');
const frequenciesContainer = node.querySelector('.frequencies'); const frequenciesContainer = node.querySelector('.frequencies');
const definitionsContainer = node.querySelector('.term-definition-list'); const definitionsContainer = node.querySelector('.term-definition-list');
const debugInfoContainer = node.querySelector('.debug-info'); 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 expressionMulti = Array.isArray(details.expressions);
const definitionMulti = Array.isArray(details.definitions); 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.expressionMulti = `${expressionMulti}`;
node.dataset.definitionMulti = `${definitionMulti}`; node.dataset.definitionMulti = `${definitionMulti}`;
node.dataset.expressionCount = `${expressionMulti ? details.expressions.length : 1}`; node.dataset.expressionCount = `${expressionCount}`;
node.dataset.definitionCount = `${definitionMulti ? details.definitions.length : 1}`; 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; const termTags = details.termTags;
let expressions = details.expressions; let expressions = details.expressions;
@ -56,6 +73,7 @@ class DisplayGenerator {
DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), expressions, [[details, termTags]]); DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), expressions, [[details, termTags]]);
DisplayGenerator._appendMultiple(reasonsContainer, this.createTermReason.bind(this), details.reasons); DisplayGenerator._appendMultiple(reasonsContainer, this.createTermReason.bind(this), details.reasons);
DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies); 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]); DisplayGenerator._appendMultiple(definitionsContainer, this.createTermDefinitionItem.bind(this), details.definitions, [details]);
if (debugInfoContainer !== null) { if (debugInfoContainer !== null) {
@ -262,6 +280,133 @@ class DisplayGenerator {
return node; 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) { createFrequencyTag(details) {
const node = this._templateHandler.instantiate('tag-frequency'); 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; } if (container === null) { return 0; }
const isArray = Array.isArray(detailsArray); const multi = (
if (!isArray) { detailsArray = fallback; } detailsIterable !== null &&
typeof detailsIterable === 'object' &&
typeof detailsIterable[Symbol.iterator] !== 'undefined'
);
if (!multi) { detailsIterable = fallback; }
container.dataset.multi = `${isArray}`; let count = 0;
container.dataset.count = `${detailsArray.length}`; for (const details of detailsIterable) {
for (const details of detailsArray) {
const item = createItem(details); const item = createItem(details);
if (item === null) { continue; } if (item === null) { continue; }
container.appendChild(item); container.appendChild(item);
++count;
} }
return detailsArray.length; container.dataset.multi = `${multi}`;
container.dataset.count = `${count}`;
return count;
} }
static _appendFurigana(container, segments, addText) { static _appendFurigana(container, segments, addText) {
@ -342,4 +493,79 @@ class DisplayGenerator {
container.appendChild(document.createTextNode(parts[i])); 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;
}
} }

View File

@ -385,6 +385,9 @@ class Display {
data.audioEnabled = `${options.audio.enabled}`; data.audioEnabled = `${options.audio.enabled}`;
data.compactGlossaries = `${options.general.compactGlossaries}`; data.compactGlossaries = `${options.general.compactGlossaries}`;
data.enableSearchTags = `${options.scanning.enableSearchTags}`; 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}`; data.debug = `${options.general.debugInfo}`;
} }

View File

@ -64,6 +64,8 @@ const jp = (() => {
[0xffe0, 0xffee] // Currency markers [0xffe0, 0xffee] // Currency markers
]; ];
const SMALL_KANA_SET = new Set(Array.from('ぁぃぅぇぉゃゅょゎァィゥェォャュョヮ'));
// Character code testing functions // 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 // Exports
return { return {
@ -119,6 +141,8 @@ const jp = (() => {
isCodePointKana, isCodePointKana,
isCodePointJapanese, isCodePointJapanese,
isStringEntirelyKana, isStringEntirelyKana,
isStringPartiallyJapanese isStringPartiallyJapanese,
isMoraPitchHigh,
getKanaMorae
}; };
})(); })();

View File

@ -0,0 +1,4 @@
[
["ptag1", "pcategory1", 0, "ptag1 notes", 0],
["ptag2", "pcategory2", 0, "ptag2 notes", 0]
]

View File

@ -1,5 +1,39 @@
[ [
["打", "freq", 1], ["打", "freq", 1],
["打つ", "freq", 2], ["打つ", "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"]}
]
}
]
] ]

View File

@ -231,8 +231,8 @@ async function testDatabase1() {
true true
); );
vm.assert.deepStrictEqual(counts, { vm.assert.deepStrictEqual(counts, {
counts: [{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: 3, tagMeta: 12} total: {kanji: 2, kanjiMeta: 2, terms: 32, termMeta: 6, tagMeta: 14}
}); });
// Test find* functions // Test find* functions
@ -648,9 +648,10 @@ async function testFindTermMetaBulk1(database, titles) {
} }
], ],
expectedResults: { expectedResults: {
total: 1, total: 3,
modes: [ modes: [
['freq', 1] ['freq', 1],
['pitch', 2]
] ]
} }
}, },

View File

@ -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() { function main() {
testIsCodePointKanji(); testIsCodePointKanji();
@ -408,6 +461,8 @@ function main() {
testConvertAlphabeticToKana(); testConvertAlphabeticToKana();
testDistributeFurigana(); testDistributeFurigana();
testDistributeFuriganaInflected(); testDistributeFuriganaInflected();
testIsMoraPitchHigh();
testGetKanaMorae();
} }