diff options
29 files changed, 1332 insertions, 107 deletions
diff --git a/.eslintrc.json b/.eslintrc.json index db8ff1fa..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" @@ -106,6 +108,7 @@ }, { "files": ["test/**/*.js"], + "excludedFiles": ["test/data/html/*.js"], "parserOptions": { "ecmaVersion": 8, "sourceType": "module" 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 }; })(); diff --git a/package.json b/package.json index 8ae103a0..b02ec179 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "test": "npm run test-lint && npm run test-code", "test-lint": "eslint . && node ./test/lint/global-declarations.js", - "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js" + "test-code": "node ./test/test-schema.js && node ./test/test-dictionary.js && node ./test/test-database.js && node ./test/test-document.js && node ./test/test-object-property-accessor.js && node ./test/test-japanese.js && node ./test/test-text-source-map.js" }, "repository": { "type": "git", 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/data/html/test-document2-frame1.html b/test/data/html/test-document2-frame1.html new file mode 100644 index 00000000..3b85cdc5 --- /dev/null +++ b/test/data/html/test-document2-frame1.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Yomichan Tests</title> + <script src="test-document2-script.js"></script> + <style> +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + padding: 0; + margin: 0; + background-color: #f8f8f8; +} +a, a:visited { + color: #1080c0; + text-decoration: underline; +} +.content { + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + padding: 0.5em; + background-color: #f8f8f8; +} + </style> + </head> +<body><div class="content"> +<div> + ありがとう +</div> +<div> + <a href="#" id="fullscreen-link">Toggle fullscreen</a> + <script> +document.querySelector('#fullscreen-link').addEventListener('click', () => toggleFullscreen(document.body), false); + </script> +</div> +</div></body> +</html>
\ No newline at end of file diff --git a/test/data/html/test-document2-script.js b/test/data/html/test-document2-script.js new file mode 100644 index 00000000..bd5a570d --- /dev/null +++ b/test/data/html/test-document2-script.js @@ -0,0 +1,41 @@ +function requestFullscreen(element) { + if (element.requestFullscreen) { + element.requestFullscreen(); + } else if (element.mozRequestFullScreen) { + element.mozRequestFullScreen(); + } else if (element.webkitRequestFullscreen) { + element.webkitRequestFullscreen(); + } else if (element.msRequestFullscreen) { + element.msRequestFullscreen(); + } +} + +function exitFullscreen() { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } +} + +function getFullscreenElement() { + return ( + document.fullscreenElement || + document.msFullscreenElement || + document.mozFullScreenElement || + document.webkitFullscreenElement || + null + ); +} + +function toggleFullscreen(element) { + if (getFullscreenElement()) { + exitFullscreen(); + } else { + requestFullscreen(element); + } +} diff --git a/test/data/html/test-document2.html b/test/data/html/test-document2.html new file mode 100644 index 00000000..3a22a5bf --- /dev/null +++ b/test/data/html/test-document2.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width,initial-scale=1" /> + <title>Yomichan Manual Tests</title> + <link rel="icon" type="image/gif" href="data:image/gif;base64,R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAImFI6Zpt0B4YkS0TCpq07xbmEgcGVRUpLaI46ZG7ppalY0jDCwUAAAOw==" /> + <link rel="stylesheet" href="test-stylesheet.css" /> + <script src="test-document2-script.js"></script> + </head> +<body> + + <h1>Yomichan Manual Tests</h1> + <p class="description">Manual tests involving fullscreen elements, <iframe>s, and shadow DOMs.</p> + + <div class="test"> + <div class="description">Standard content.</div> + <div id="fullscreen-element1" style="width: 100%; height: 200px; border: 1px solid #d8d8d8; position: relative;"><div style="background-color: #f8f8f8; padding: 0.5em; position: absolute; left: 0; top: 0; bottom: 0; right: 0;"> + <div> + ありがとう + </div> + <div> + <a href="#" id="fullscreen-link1">Toggle fullscreen</a> + </div> + </div></div> + <script> +document.querySelector('#fullscreen-link1').addEventListener('click', () => toggleFullscreen(document.querySelector('#fullscreen-element1')), false); + </script> + </div> + + <div class="test"> + <div class="description">Content inside of a shadow DOM.</div> + <div id="shadow-content-container"></div> + <template id="shadow-content-container-content-template"> + <link rel="stylesheet" href="test-stylesheet.css" /> + <div id="fullscreen-element2" style="width: 100%; height: 200px; border: 1px solid #d8d8d8; position: relative;"><div style="background-color: #f8f8f8; padding: 0.5em; position: absolute; left: 0; top: 0; bottom: 0; right: 0;"> + <div> + ありがとう + </div> + <div> + <a href="#" id="fullscreen-link2">Toggle fullscreen</a> + </div> + </div></div> + </template> + <script> +(() => { + const shadowIframeContainer = document.querySelector('#shadow-content-container'); + const shadow = shadowIframeContainer.attachShadow({mode: 'closed'}); + const template = document.querySelector('#shadow-content-container-content-template').content; + const content = document.importNode(template, true); + const fullscreenElement = content.querySelector('#fullscreen-element2'); + content.querySelector('#fullscreen-link2').addEventListener('click', () => toggleFullscreen(fullscreenElement), false); + shadow.appendChild(content); +})(); + </script> + </div> + + <div class="test"> + <div class="description"><iframe> element.</div> + <iframe src="test-document2-frame1.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe> + </div> + + <div class="test"> + <div class="description"><iframe> element inside of a shadow DOM.</div> + <div id="shadow-iframe-container"></div> + <template id="shadow-iframe-container-content-template"> + <iframe src="test-document2-frame1.html" allowfullscreen="true" style="width: 100%; height: 200px; border: 1px solid #d8d8d8;"></iframe> + </template> + <script> +(() => { + const shadowIframeContainer = document.querySelector('#shadow-iframe-container'); + const shadow = shadowIframeContainer.attachShadow({mode: 'closed'}); + const template = document.querySelector('#shadow-iframe-container-content-template').content; + const content = document.importNode(template, true); + shadow.appendChild(content); +})(); + </script> + </div> + +</body> +</html>
\ No newline at end of file diff --git a/test/data/html/test-stylesheet.css b/test/data/html/test-stylesheet.css index ab25732e..f63d2481 100644 --- a/test/data/html/test-stylesheet.css +++ b/test/data/html/test-stylesheet.css @@ -7,6 +7,7 @@ body { margin: 0 auto; background-color: #f8f8f8; counter-reset: test-id; + overflow-y: scroll; } h1 { @@ -14,6 +15,19 @@ h1 { margin: 0.67em 0; } +p { + margin: 0.33em 0; +} + +h1+p { + margin-top: -0.67em; +} + +a, a:visited { + color: #1080c0; + text-decoration: underline; +} + .test { background-color: #ffffff; margin: 1em 0; @@ -30,3 +44,8 @@ h1 { border-bottom: 1px solid #d8d8d8; font-weight: bold; } + +.description { + color: #444444; + font-style: italic; +} diff --git a/test/test-database.js b/test/test-database.js index c3402b73..c4bfb793 100644 --- a/test/test-database.js +++ b/test/test-database.js @@ -235,8 +235,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 @@ -652,9 +652,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..ca65dde2 100644 --- a/test/test-japanese.js +++ b/test/test-japanese.js @@ -23,9 +23,11 @@ const vm = new VM(); vm.execute([ 'mixed/lib/wanakana.min.js', 'mixed/js/japanese.js', + 'bg/js/text-source-map.js', 'bg/js/japanese.js' ]); const jp = vm.get('jp'); +const TextSourceMap = vm.get('TextSourceMap'); function testIsCodePointKanji() { @@ -262,13 +264,13 @@ function testConvertHalfWidthKanaToFullWidth() { ]; for (const [string, expected, expectedSourceMapping] of data) { - const sourceMapping = new Array(string.length).fill(1); + const sourceMap = new TextSourceMap(string); const actual1 = jp.convertHalfWidthKanaToFullWidth(string, null); - const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMapping); + const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMap); assert.strictEqual(actual1, expected); assert.strictEqual(actual2, expected); - if (Array.isArray(expectedSourceMapping)) { - vm.assert.deepStrictEqual(sourceMapping, expectedSourceMapping); + if (typeof expectedSourceMapping !== 'undefined') { + assert.ok(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))); } } } @@ -285,13 +287,13 @@ function testConvertAlphabeticToKana() { ]; for (const [string, expected, expectedSourceMapping] of data) { - const sourceMapping = new Array(string.length).fill(1); + const sourceMap = new TextSourceMap(string); const actual1 = jp.convertAlphabeticToKana(string, null); - const actual2 = jp.convertAlphabeticToKana(string, sourceMapping); + const actual2 = jp.convertAlphabeticToKana(string, sourceMap); assert.strictEqual(actual1, expected); assert.strictEqual(actual2, expected); - if (Array.isArray(expectedSourceMapping)) { - vm.assert.deepStrictEqual(sourceMapping, expectedSourceMapping); + if (typeof expectedSourceMapping !== 'undefined') { + assert.ok(sourceMap.equals(new TextSourceMap(string, expectedSourceMapping))); } } } @@ -392,6 +394,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 +463,8 @@ function main() { testConvertAlphabeticToKana(); testDistributeFurigana(); testDistributeFuriganaInflected(); + testIsMoraPitchHigh(); + testGetKanaMorae(); } diff --git a/test/test-text-source-map.js b/test/test-text-source-map.js new file mode 100644 index 00000000..25bd8fc2 --- /dev/null +++ b/test/test-text-source-map.js @@ -0,0 +1,234 @@ +/* + * 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/>. + */ + +const assert = require('assert'); +const {VM} = require('./yomichan-vm'); + +const vm = new VM(); +vm.execute(['bg/js/text-source-map.js']); +const TextSourceMap = vm.get('TextSourceMap'); + + +function testSource() { + const data = [ + ['source1'], + ['source2'], + ['source3'] + ]; + + for (const [source] of data) { + const sourceMap = new TextSourceMap(source); + assert.strictEqual(source, sourceMap.source); + } +} + +function testEquals() { + const data = [ + [['source1', null], ['source1', null], true], + [['source2', null], ['source2', null], true], + [['source3', null], ['source3', null], true], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', null], true], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', null], true], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', null], true], + + [['source1', null], ['source1', [1, 1, 1, 1, 1, 1, 1]], true], + [['source2', null], ['source2', [1, 1, 1, 1, 1, 1, 1]], true], + [['source3', null], ['source3', [1, 1, 1, 1, 1, 1, 1]], true], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source1', [1, 1, 1, 1, 1, 1, 1]], true], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source2', [1, 1, 1, 1, 1, 1, 1]], true], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source3', [1, 1, 1, 1, 1, 1, 1]], true], + + [['source1', [1, 2, 1, 3]], ['source1', [1, 2, 1, 3]], true], + [['source2', [1, 2, 1, 3]], ['source2', [1, 2, 1, 3]], true], + [['source3', [1, 2, 1, 3]], ['source3', [1, 2, 1, 3]], true], + + [['source1', [1, 3, 1, 2]], ['source1', [1, 2, 1, 3]], false], + [['source2', [1, 3, 1, 2]], ['source2', [1, 2, 1, 3]], false], + [['source3', [1, 3, 1, 2]], ['source3', [1, 2, 1, 3]], false], + + [['source1', [1, 1, 1, 1, 1, 1, 1]], ['source4', [1, 1, 1, 1, 1, 1, 1]], false], + [['source2', [1, 1, 1, 1, 1, 1, 1]], ['source5', [1, 1, 1, 1, 1, 1, 1]], false], + [['source3', [1, 1, 1, 1, 1, 1, 1]], ['source6', [1, 1, 1, 1, 1, 1, 1]], false] + ]; + + for (const [[source1, mapping1], [source2, mapping2], expectedEquals] of data) { + const sourceMap1 = new TextSourceMap(source1, mapping1); + const sourceMap2 = new TextSourceMap(source2, mapping2); + assert.ok(sourceMap1.equals(sourceMap1)); + assert.ok(sourceMap2.equals(sourceMap2)); + assert.strictEqual(sourceMap1.equals(sourceMap2), expectedEquals); + } +} + +function testGetSourceLength() { + const data = [ + [['source', [1, 1, 1, 1, 1, 1]], 1, 1], + [['source', [1, 1, 1, 1, 1, 1]], 2, 2], + [['source', [1, 1, 1, 1, 1, 1]], 3, 3], + [['source', [1, 1, 1, 1, 1, 1]], 4, 4], + [['source', [1, 1, 1, 1, 1, 1]], 5, 5], + [['source', [1, 1, 1, 1, 1, 1]], 6, 6], + + [['source', [2, 2, 2]], 1, 2], + [['source', [2, 2, 2]], 2, 4], + [['source', [2, 2, 2]], 3, 6], + + [['source', [3, 3]], 1, 3], + [['source', [3, 3]], 2, 6], + + [['source', [6, 6]], 1, 6] + ]; + + for (const [[source, mapping], finalLength, expectedValue] of data) { + const sourceMap = new TextSourceMap(source, mapping); + assert.strictEqual(sourceMap.getSourceLength(finalLength), expectedValue); + } +} + +function testCombineInsert() { + const data = [ + // No operations + [ + ['source', null], + ['source', [1, 1, 1, 1, 1, 1]], + [] + ], + + // Combine + [ + ['source', null], + ['source', [3, 1, 1, 1]], + [ + ['combine', 0, 2] + ] + ], + [ + ['source', null], + ['source', [1, 1, 1, 3]], + [ + ['combine', 3, 2] + ] + ], + [ + ['source', null], + ['source', [3, 3]], + [ + ['combine', 0, 2], + ['combine', 1, 2] + ] + ], + [ + ['source', null], + ['source', [3, 3]], + [ + ['combine', 3, 2], + ['combine', 0, 2] + ] + ], + + // Insert + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1]], + [ + ['insert', 0, 0] + ] + ], + [ + ['source', null], + ['source', [1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 6, 0] + ] + ], + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 0, 0], + ['insert', 7, 0] + ] + ], + [ + ['source', null], + ['source', [0, 1, 1, 1, 1, 1, 1, 0]], + [ + ['insert', 6, 0], + ['insert', 0, 0] + ] + ], + + // Mixed + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['combine', 0, 2], + ['insert', 1, 0], + ['combine', 2, 2] + ] + ], + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['combine', 0, 2], + ['combine', 1, 2], + ['insert', 1, 0] + ] + ], + [ + ['source', null], + ['source', [3, 0, 3]], + [ + ['insert', 3, 0], + ['combine', 0, 2], + ['combine', 2, 2] + ] + ] + ]; + + for (const [[source, mapping], [expectedSource, expectedMapping], operations] of data) { + const sourceMap = new TextSourceMap(source, mapping); + const expectedSourceMap = new TextSourceMap(expectedSource, expectedMapping); + for (const [operation, ...args] of operations) { + switch (operation) { + case 'combine': + sourceMap.combine(...args); + break; + case 'insert': + sourceMap.insert(...args); + break; + } + } + assert.ok(sourceMap.equals(expectedSourceMap)); + } +} + + +function main() { + testSource(); + testEquals(); + testGetSourceLength(); + testCombineInsert(); +} + + +if (require.main === module) { main(); } |