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