diff options
Diffstat (limited to 'ext')
-rw-r--r-- | ext/bg/background.html | 1 | ||||
-rw-r--r-- | ext/bg/data/dictionary-term-meta-bank-v3-schema.json | 64 | ||||
-rw-r--r-- | ext/bg/data/options-schema.json | 17 | ||||
-rw-r--r-- | ext/bg/js/dictionary.js | 32 | ||||
-rw-r--r-- | ext/bg/js/japanese.js | 31 | ||||
-rw-r--r-- | ext/bg/js/options.js | 5 | ||||
-rw-r--r-- | ext/bg/js/settings/main.js | 6 | ||||
-rw-r--r-- | ext/bg/js/text-source-map.js | 115 | ||||
-rw-r--r-- | ext/bg/js/translator.js | 52 | ||||
-rw-r--r-- | ext/bg/settings.html | 12 | ||||
-rw-r--r-- | ext/mixed/css/display-dark.css | 27 | ||||
-rw-r--r-- | ext/mixed/css/display-default.css | 27 | ||||
-rw-r--r-- | ext/mixed/css/display.css | 190 | ||||
-rw-r--r-- | ext/mixed/display-templates.html | 17 | ||||
-rw-r--r-- | ext/mixed/js/core.js | 24 | ||||
-rw-r--r-- | ext/mixed/js/display-generator.js | 246 | ||||
-rw-r--r-- | ext/mixed/js/display.js | 3 | ||||
-rw-r--r-- | ext/mixed/js/japanese.js | 26 |
18 files changed, 802 insertions, 93 deletions
diff --git a/ext/bg/background.html b/ext/bg/background.html index 62802341..afe9c5d1 100644 --- a/ext/bg/background.html +++ b/ext/bg/background.html @@ -39,6 +39,7 @@ <script src="/bg/js/options.js"></script> <script src="/bg/js/profile-conditions.js"></script> <script src="/bg/js/request.js"></script> + <script src="/bg/js/text-source-map.js"></script> <script src="/bg/js/translator.js"></script> <script src="/bg/js/util.js"></script> <script src="/mixed/js/audio-system.js"></script> 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/japanese.js b/ext/bg/js/japanese.js index c5873cf1..2a2b39fd 100644 --- a/ext/bg/js/japanese.js +++ b/ext/bg/js/japanese.js @@ -158,9 +158,8 @@ return result; } - function convertHalfWidthKanaToFullWidth(text, sourceMapping) { + function convertHalfWidthKanaToFullWidth(text, sourceMap=null) { let result = ''; - const hasSourceMapping = Array.isArray(sourceMapping); // This function is safe to use charCodeAt instead of codePointAt, since all // the relevant characters are represented with a single UTF-16 character code. @@ -192,10 +191,8 @@ } } - if (hasSourceMapping && index > 0) { - index = result.length; - const v = sourceMapping.splice(index + 1, 1)[0]; - sourceMapping[index] += v; + if (sourceMap !== null && index > 0) { + sourceMap.combine(result.length, 1); } result += c2; } @@ -203,7 +200,7 @@ return result; } - function convertAlphabeticToKana(text, sourceMapping) { + function convertAlphabeticToKana(text, sourceMap=null) { let part = ''; let result = ''; @@ -222,7 +219,7 @@ c = 0x2d; // '-' } else { if (part.length > 0) { - result += convertAlphabeticPartToKana(part, sourceMapping, result.length); + result += convertAlphabeticPartToKana(part, sourceMap, result.length); part = ''; } result += char; @@ -232,17 +229,16 @@ } if (part.length > 0) { - result += convertAlphabeticPartToKana(part, sourceMapping, result.length); + result += convertAlphabeticPartToKana(part, sourceMap, result.length); } return result; } - function convertAlphabeticPartToKana(text, sourceMapping, sourceMappingStart) { + function convertAlphabeticPartToKana(text, sourceMap, sourceMapStart) { const result = wanakana.toHiragana(text); // Generate source mapping - if (Array.isArray(sourceMapping)) { - if (typeof sourceMappingStart !== 'number') { sourceMappingStart = 0; } + if (sourceMap !== null) { let i = 0; let resultPos = 0; const ii = text.length; @@ -262,18 +258,15 @@ // Merge characters const removals = iNext - i - 1; if (removals > 0) { - let sum = 0; - const vs = sourceMapping.splice(sourceMappingStart + 1, removals); - for (const v of vs) { sum += v; } - sourceMapping[sourceMappingStart] += sum; + sourceMap.combine(sourceMapStart, removals); } - ++sourceMappingStart; + ++sourceMapStart; // Empty elements const additions = resultPosNext - resultPos - 1; for (let j = 0; j < additions; ++j) { - sourceMapping.splice(sourceMappingStart, 0, 0); - ++sourceMappingStart; + sourceMap.insert(sourceMapStart, 0); + ++sourceMapStart; } i = iNext; 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/text-source-map.js b/ext/bg/js/text-source-map.js new file mode 100644 index 00000000..24970978 --- /dev/null +++ b/ext/bg/js/text-source-map.js @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net> + * Author: Alex Yatskov <alex@foosoft.net> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +class TextSourceMap { + constructor(source, mapping=null) { + this._source = source; + this._mapping = (Array.isArray(mapping) ? TextSourceMap._normalizeMapping(mapping) : null); + } + + get source() { + return this._source; + } + + equals(other) { + if (this === other) { + return true; + } + + const source = this._source; + if (!(other instanceof TextSourceMap && source === other._source)) { + return false; + } + + let mapping = this._mapping; + let otherMapping = other._mapping; + if (mapping === null) { + if (otherMapping === null) { + return true; + } + mapping = TextSourceMap._createMapping(source); + } else if (otherMapping === null) { + otherMapping = TextSourceMap._createMapping(source); + } + + const mappingLength = mapping.length; + if (mappingLength !== otherMapping.length) { + return false; + } + + for (let i = 0; i < mappingLength; ++i) { + if (mapping[i] !== otherMapping[i]) { + return false; + } + } + + return true; + } + + getSourceLength(finalLength) { + const mapping = this._mapping; + if (mapping === null) { + return finalLength; + } + + let sourceLength = 0; + for (let i = 0; i < finalLength; ++i) { + sourceLength += mapping[i]; + } + return sourceLength; + } + + combine(index, count) { + if (count <= 0) { return; } + + if (this._mapping === null) { + this._mapping = TextSourceMap._createMapping(this._source); + } + + let sum = this._mapping[index]; + const parts = this._mapping.splice(index + 1, count); + for (const part of parts) { + sum += part; + } + this._mapping[index] = sum; + } + + insert(index, ...items) { + if (this._mapping === null) { + this._mapping = TextSourceMap._createMapping(this._source); + } + + this._mapping.splice(index, 0, ...items); + } + + static _createMapping(text) { + return new Array(text.length).fill(1); + } + + static _normalizeMapping(mapping) { + const result = []; + for (const value of mapping) { + result.push( + (typeof value === 'number' && Number.isFinite(value)) ? + Math.floor(value) : + 0 + ); + } + return result; + } +} diff --git a/ext/bg/js/translator.js b/ext/bg/js/translator.js index df19eee1..27f91c05 100644 --- a/ext/bg/js/translator.js +++ b/ext/bg/js/translator.js @@ -18,6 +18,7 @@ /* global * Deinflector + * TextSourceMap * dictEnabledSet * dictTagBuildSource * dictTagSanitize @@ -359,17 +360,15 @@ class Translator { const used = new Set(); for (const [halfWidth, numeric, alphabetic, katakana, hiragana] of Translator.getArrayVariants(textOptionVariantArray)) { let text2 = text; - let sourceMapping = null; + const sourceMap = new TextSourceMap(text2); if (halfWidth) { - if (sourceMapping === null) { sourceMapping = Translator.createTextSourceMapping(text2); } - text2 = jp.convertHalfWidthKanaToFullWidth(text2, sourceMapping); + text2 = jp.convertHalfWidthKanaToFullWidth(text2, sourceMap); } if (numeric) { text2 = jp.convertNumericToFullWidth(text2); } if (alphabetic) { - if (sourceMapping === null) { sourceMapping = Translator.createTextSourceMapping(text2); } - text2 = jp.convertAlphabeticToKana(text2, sourceMapping); + text2 = jp.convertAlphabeticToKana(text2, sourceMap); } if (katakana) { text2 = jp.convertHiraganaToKatakana(text2); @@ -383,7 +382,7 @@ class Translator { if (used.has(text2Substring)) { break; } used.add(text2Substring); for (const deinflection of this.deinflector.deinflect(text2Substring)) { - deinflection.rawSource = Translator.getDeinflectionRawSource(text, i, sourceMapping); + deinflection.rawSource = sourceMap.source.substring(0, sourceMap.getSourceLength(i)); deinflections.push(deinflection); } } @@ -399,25 +398,6 @@ class Translator { } } - static getDeinflectionRawSource(source, length, sourceMapping) { - if (sourceMapping === null) { - return source.substring(0, length); - } - - let result = ''; - let index = 0; - for (let i = 0; i < length; ++i) { - const c = sourceMapping[i]; - result += source.substring(index, index + c); - index += c; - } - return result; - } - - static createTextSourceMapping(text) { - return new Array(text.length).fill(1); - } - async findKanji(text, options) { const dictionaries = dictEnabledSet(options); const kanjiUnique = new Set(); @@ -482,6 +462,7 @@ class Translator { // New data term.frequencies = []; + term.pitches = []; } const metas = await this.database.findTermMetaBulk(expressionsUnique, dictionaries); @@ -492,6 +473,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; } } } @@ -575,6 +563,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 }; })(); |