summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/data/anki-note-builder-test-results.json48
-rw-r--r--test/data/html/test-document2-script.js40
-rw-r--r--test/data/translator-test-results-note-data1.json280
-rw-r--r--test/data/translator-test-results.json236
-rw-r--r--test/dictionary.test.js6
-rw-r--r--test/jsconfig.json39
-rw-r--r--test/playwright/visual.spec.js12
-rw-r--r--test/test-all.js71
-rw-r--r--test/test-anki-note-builder.js322
-rw-r--r--test/test-cache-map.js137
-rw-r--r--test/test-core.js300
-rw-r--r--test/test-database.js982
-rw-r--r--test/test-document-util.js339
-rw-r--r--test/test-hotkey-util.js189
-rw-r--r--test/test-japanese-util.js915
-rw-r--r--test/test-json-schema.js1048
-rw-r--r--test/test-manifest.js49
-rw-r--r--test/test-object-property-accessor.js458
-rw-r--r--test/test-profile-conditions-util.js1136
-rw-r--r--test/test-text-source-map.js244
-rw-r--r--test/test-translator.js102
-rw-r--r--test/test-workers.js168
22 files changed, 6833 insertions, 288 deletions
diff --git a/test/data/anki-note-builder-test-results.json b/test/data/anki-note-builder-test-results.json
index b752e878..49542e39 100644
--- a/test/data/anki-note-builder-test-results.json
+++ b/test/data/anki-note-builder-test-results.json
@@ -194,7 +194,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>つ",
"furigana-plain": "打[う]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>",
@@ -224,7 +224,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>つ",
"furigana-plain": "打[ぶ]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>",
@@ -379,7 +379,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[う]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>",
@@ -409,7 +409,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[ぶ]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>",
@@ -499,7 +499,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>つ",
"furigana-plain": "打[う]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>",
@@ -529,7 +529,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>つ",
"furigana-plain": "打[ぶ]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>",
@@ -759,7 +759,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>つ",
"furigana-plain": "打[う]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>",
@@ -824,7 +824,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>つ",
"furigana-plain": "打[ぶ]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>",
@@ -889,7 +889,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[う]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>",
@@ -949,7 +949,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>つ",
"furigana-plain": "打[う]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>",
@@ -1014,7 +1014,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[ぶ]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>",
@@ -1074,7 +1074,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>つ",
"furigana-plain": "打[ぶ]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>",
@@ -1526,7 +1526,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[う]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>",
@@ -1556,7 +1556,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[ぶ]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>",
@@ -1646,7 +1646,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>つ",
"furigana-plain": "打[う]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>",
@@ -1676,7 +1676,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>つ",
"furigana-plain": "打[ぶ]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>",
@@ -1831,7 +1831,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[う]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>",
@@ -1861,7 +1861,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[ぶ]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>",
@@ -1951,7 +1951,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>つ",
"furigana-plain": "打[う]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>",
@@ -1981,7 +1981,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>つ",
"furigana-plain": "打[ぶ]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>",
@@ -2136,7 +2136,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 24</li><li>Test Dictionary 2: 30</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 12</li><li>Test Dictionary 2: eighteen</li><li>Test Dictionary 2: twenty-four (24)</li><li>Test Dictionary 2: 30</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[う]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>uchikomu definition 3</li><li>uchikomu definition 4</li></ul></div>",
@@ -2166,7 +2166,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打ち込む",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: 7</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 25</li><li>Test Dictionary 2: 31</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 3</li><li>Test Dictionary 2: seven</li><li>Test Dictionary 2: 13</li><li>Test Dictionary 2: nineteen</li><li>Test Dictionary 2: twenty-five (25)</li><li>Test Dictionary 2: thirty-one</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>ち<ruby>込<rt>こ</rt></ruby>む",
"furigana-plain": "打[ぶ]ち 込[こ]む",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>buchikomu definition 3</li><li>buchikomu definition 4</li></ul></div>",
@@ -2256,7 +2256,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 22</li><li>Test Dictionary 2: 28</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 10</li><li>Test Dictionary 2: sixteen</li><li>Test Dictionary 2: twenty-two (22)</li><li>Test Dictionary 2: 28</li></ul>",
"furigana": "<ruby>打<rt>う</rt></ruby>つ",
"furigana-plain": "打[う]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>utsu definition 3</li><li>utsu definition 4</li></ul></div>",
@@ -2286,7 +2286,7 @@
"dictionary": "Test Dictionary 2",
"document-title": "title",
"expression": "打つ",
- "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: 0</li><li>Test Dictionary 2: 23</li><li>Test Dictionary 2: 29</li></ul>",
+ "frequencies": "<ul style=\"text-align: left;\"><li>Test Dictionary 2: 2</li><li>Test Dictionary 2: 6</li><li>Test Dictionary 2: 11</li><li>Test Dictionary 2: seventeen</li><li>Test Dictionary 2: twenty-three (23)</li><li>Test Dictionary 2: twenty-nine</li></ul>",
"furigana": "<ruby>打<rt>ぶ</rt></ruby>つ",
"furigana-plain": "打[ぶ]つ",
"glossary": "<div style=\"text-align: left;\"><i>(vt, Test Dictionary 2)</i> <ul><li>butsu definition 3</li><li>butsu definition 4</li></ul></div>",
diff --git a/test/data/html/test-document2-script.js b/test/data/html/test-document2-script.js
index 8a183019..5a6ad4d1 100644
--- a/test/data/html/test-document2-script.js
+++ b/test/data/html/test-document2-script.js
@@ -16,40 +16,65 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/**
+ * @param {Element} element
+ */
function requestFullscreen(element) {
if (element.requestFullscreen) {
element.requestFullscreen();
+ // @ts-ignore - Browser compatibility
} else if (element.mozRequestFullScreen) {
+ // @ts-ignore - Browser compatibility
element.mozRequestFullScreen();
+ // @ts-ignore - Browser compatibility
} else if (element.webkitRequestFullscreen) {
+ // @ts-ignore - Browser compatibility
element.webkitRequestFullscreen();
+ // @ts-ignore - Browser compatibility
} else if (element.msRequestFullscreen) {
+ // @ts-ignore - Browser compatibility
element.msRequestFullscreen();
}
}
+/** */
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
+ // @ts-ignore - Browser compatibility
} else if (document.mozCancelFullScreen) {
+ // @ts-ignore - Browser compatibility
document.mozCancelFullScreen();
+ // @ts-ignore - Browser compatibility
} else if (document.webkitExitFullscreen) {
+ // @ts-ignore - Browser compatibility
document.webkitExitFullscreen();
+ // @ts-ignore - Browser compatibility
} else if (document.msExitFullscreen) {
+ // @ts-ignore - Browser compatibility
document.msExitFullscreen();
}
}
+/**
+ * @returns {?Element}
+ */
function getFullscreenElement() {
return (
document.fullscreenElement ||
+ // @ts-ignore - Browser compatibility
document.msFullscreenElement ||
+ // @ts-ignore - Browser compatibility
document.mozFullScreenElement ||
+ // @ts-ignore - Browser compatibility
document.webkitFullscreenElement ||
null
);
}
+/**
+ * @param {Element} element
+ */
function toggleFullscreen(element) {
if (getFullscreenElement()) {
exitFullscreen();
@@ -58,6 +83,10 @@ function toggleFullscreen(element) {
}
}
+/**
+ * @param {HTMLElement|DocumentFragment} container
+ * @param {?Element} [fullscreenElement]
+ */
function setup(container, fullscreenElement=null) {
const fullscreenLink = container.querySelector('.fullscreen-link');
if (fullscreenLink !== null) {
@@ -65,6 +94,7 @@ function setup(container, fullscreenElement=null) {
fullscreenElement = container.querySelector('.fullscreen-element');
}
fullscreenLink.addEventListener('click', (e) => {
+ if (fullscreenElement === null) { return; }
toggleFullscreen(fullscreenElement);
e.preventDefault();
return false;
@@ -74,11 +104,15 @@ function setup(container, fullscreenElement=null) {
const template = container.querySelector('template');
const templateContentContainer = container.querySelector('.template-content-container');
if (template !== null && templateContentContainer !== null) {
- const mode = container.dataset.shadowMode;
- const shadow = templateContentContainer.attachShadow({mode});
+ const mode = (container instanceof HTMLElement ? container.dataset.shadowMode : void 0);
+ const shadow = templateContentContainer.attachShadow({
+ mode: (mode === 'open' || mode === 'closed' ? mode : 'open')
+ });
const containerStyles = document.querySelector('#container-styles');
- shadow.appendChild(containerStyles.cloneNode(true));
+ if (containerStyles !== null) {
+ shadow.appendChild(containerStyles.cloneNode(true));
+ }
const content = document.importNode(template.content, true);
setup(content);
diff --git a/test/data/translator-test-results-note-data1.json b/test/data/translator-test-results-note-data1.json
index d686563c..34f7c21a 100644
--- a/test/data/translator-test-results-note-data1.json
+++ b/test/data/translator-test-results-note-data1.json
@@ -1659,7 +1659,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -1672,7 +1672,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -1791,7 +1791,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -1804,7 +1804,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -1963,7 +1963,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -1976,7 +1976,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -1989,7 +1989,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -2095,7 +2095,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -2108,7 +2108,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -2121,7 +2121,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -3651,7 +3651,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -3677,7 +3677,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -3690,7 +3690,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -3813,7 +3813,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -3839,7 +3839,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -3852,7 +3852,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -4045,7 +4045,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -4071,7 +4071,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -4084,7 +4084,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -4097,7 +4097,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -4207,7 +4207,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -4233,7 +4233,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -4246,7 +4246,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -4259,7 +4259,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -5079,7 +5079,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -5092,7 +5092,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -5211,7 +5211,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -5224,7 +5224,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -5385,7 +5385,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -5398,7 +5398,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -5411,7 +5411,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -5517,7 +5517,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -5530,7 +5530,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -5543,7 +5543,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -7394,7 +7394,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -7407,7 +7407,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -7526,7 +7526,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -7539,7 +7539,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -8007,7 +8007,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -8020,7 +8020,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -8033,7 +8033,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -8139,7 +8139,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -8152,7 +8152,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -8165,7 +8165,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -8684,7 +8684,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -8710,7 +8710,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -8723,7 +8723,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -8846,7 +8846,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -8872,7 +8872,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -8885,7 +8885,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -9412,7 +9412,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -9425,7 +9425,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -9544,7 +9544,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -9557,7 +9557,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -10089,7 +10089,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -10115,7 +10115,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -10128,7 +10128,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -10141,7 +10141,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -10251,7 +10251,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -10277,7 +10277,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -10290,7 +10290,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -10303,7 +10303,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -10817,7 +10817,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -10830,7 +10830,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -10843,7 +10843,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -10949,7 +10949,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -10962,7 +10962,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -10975,7 +10975,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -16097,7 +16097,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -16123,7 +16123,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -16136,7 +16136,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -16259,7 +16259,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -16285,7 +16285,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -16298,7 +16298,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -16495,7 +16495,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -16521,7 +16521,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -16534,7 +16534,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -16547,7 +16547,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -16657,7 +16657,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -16683,7 +16683,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -16696,7 +16696,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -16709,7 +16709,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -17529,7 +17529,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -17542,7 +17542,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -17661,7 +17661,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -17674,7 +17674,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -17835,7 +17835,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -17848,7 +17848,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -17861,7 +17861,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -17967,7 +17967,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -17980,7 +17980,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -17993,7 +17993,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -19523,7 +19523,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -19549,7 +19549,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -19562,7 +19562,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -19685,7 +19685,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -19711,7 +19711,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -19724,7 +19724,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -19917,7 +19917,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -19943,7 +19943,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -19956,7 +19956,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -19969,7 +19969,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -20079,7 +20079,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -20105,7 +20105,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -20118,7 +20118,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -20131,7 +20131,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -20951,7 +20951,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -20964,7 +20964,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -21083,7 +21083,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -21096,7 +21096,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -21257,7 +21257,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -21270,7 +21270,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -21283,7 +21283,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -21389,7 +21389,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -21402,7 +21402,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -21415,7 +21415,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -22945,7 +22945,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -22971,7 +22971,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -22984,7 +22984,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -23107,7 +23107,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -23133,7 +23133,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "eighteen"
},
{
"index": 4,
@@ -23146,7 +23146,7 @@
"expression": "打ち込む",
"reading": "うちこむ",
"hasReading": true,
- "frequency": 24
+ "frequency": "twenty-four (24)"
},
{
"index": 5,
@@ -23339,7 +23339,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -23365,7 +23365,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -23378,7 +23378,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -23391,7 +23391,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -23501,7 +23501,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": false,
- "frequency": 7
+ "frequency": "seven"
},
{
"index": 2,
@@ -23527,7 +23527,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 0
+ "frequency": "nineteen"
},
{
"index": 4,
@@ -23540,7 +23540,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 25
+ "frequency": "twenty-five (25)"
},
{
"index": 5,
@@ -23553,7 +23553,7 @@
"expression": "打ち込む",
"reading": "ぶちこむ",
"hasReading": true,
- "frequency": 31
+ "frequency": "thirty-one"
}
],
"pitches": [
@@ -24373,7 +24373,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -24386,7 +24386,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -24505,7 +24505,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "sixteen"
},
{
"index": 4,
@@ -24518,7 +24518,7 @@
"expression": "打つ",
"reading": "うつ",
"hasReading": true,
- "frequency": 22
+ "frequency": "twenty-two (22)"
},
{
"index": 5,
@@ -24679,7 +24679,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -24692,7 +24692,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -24705,7 +24705,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
@@ -24811,7 +24811,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 0
+ "frequency": "seventeen"
},
{
"index": 4,
@@ -24824,7 +24824,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 23
+ "frequency": "twenty-three (23)"
},
{
"index": 5,
@@ -24837,7 +24837,7 @@
"expression": "打つ",
"reading": "ぶつ",
"hasReading": true,
- "frequency": 29
+ "frequency": "twenty-nine"
}
],
"pitches": [],
diff --git a/test/data/translator-test-results.json b/test/data/translator-test-results.json
index 98db0ef4..0a7155b8 100644
--- a/test/data/translator-test-results.json
+++ b/test/data/translator-test-results.json
@@ -1101,8 +1101,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "sixteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -1112,8 +1112,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 22,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-two (22)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -1266,8 +1266,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "seventeen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -1277,8 +1277,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 23,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-three (23)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -1288,7 +1288,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 29,
- "displayValue": null,
+ "displayValue": "twenty-nine",
"displayValueParsed": false
}
]
@@ -2150,7 +2150,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -2172,8 +2172,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "eighteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -2183,8 +2183,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 24,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-four (24)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -2337,7 +2337,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -2359,8 +2359,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "nineteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -2370,8 +2370,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 25,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-five (25)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -2381,7 +2381,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 31,
- "displayValue": null,
+ "displayValue": "thirty-one",
"displayValueParsed": false
}
]
@@ -2860,8 +2860,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "sixteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -2871,8 +2871,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 22,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-two (22)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -3027,8 +3027,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "seventeen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -3038,8 +3038,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 23,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-three (23)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -3049,7 +3049,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 29,
- "displayValue": null,
+ "displayValue": "twenty-nine",
"displayValueParsed": false
}
]
@@ -4166,8 +4166,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "sixteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -4177,8 +4177,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 22,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-two (22)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -4502,8 +4502,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "seventeen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -4513,8 +4513,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 23,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-three (23)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -4524,7 +4524,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 29,
- "displayValue": null,
+ "displayValue": "twenty-nine",
"displayValueParsed": false
}
]
@@ -4860,7 +4860,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -4882,8 +4882,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "eighteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -4893,8 +4893,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 24,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-four (24)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -5216,8 +5216,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "sixteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -5227,8 +5227,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 22,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-two (22)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -5574,7 +5574,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -5596,8 +5596,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "nineteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -5607,8 +5607,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 25,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-five (25)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -5618,7 +5618,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 31,
- "displayValue": null,
+ "displayValue": "thirty-one",
"displayValueParsed": false
}
]
@@ -5930,8 +5930,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "seventeen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -5941,8 +5941,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 23,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-three (23)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -5952,7 +5952,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 29,
- "displayValue": null,
+ "displayValue": "twenty-nine",
"displayValueParsed": false
}
]
@@ -9645,7 +9645,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -9667,8 +9667,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "eighteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -9678,8 +9678,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 24,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-four (24)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -9836,7 +9836,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -9858,8 +9858,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "nineteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -9869,8 +9869,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 25,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-five (25)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -9880,7 +9880,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 31,
- "displayValue": null,
+ "displayValue": "thirty-one",
"displayValueParsed": false
}
]
@@ -10359,8 +10359,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "sixteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -10370,8 +10370,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 22,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-two (22)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -10526,8 +10526,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "seventeen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -10537,8 +10537,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 23,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-three (23)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -10548,7 +10548,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 29,
- "displayValue": null,
+ "displayValue": "twenty-nine",
"displayValueParsed": false
}
]
@@ -11410,7 +11410,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -11432,8 +11432,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "eighteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -11443,8 +11443,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 24,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-four (24)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -11597,7 +11597,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -11619,8 +11619,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "nineteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -11630,8 +11630,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 25,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-five (25)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -11641,7 +11641,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 31,
- "displayValue": null,
+ "displayValue": "thirty-one",
"displayValueParsed": false
}
]
@@ -12120,8 +12120,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "sixteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -12131,8 +12131,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 22,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-two (22)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -12287,8 +12287,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "seventeen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -12298,8 +12298,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 23,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-three (23)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -12309,7 +12309,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 29,
- "displayValue": null,
+ "displayValue": "twenty-nine",
"displayValueParsed": false
}
]
@@ -13171,7 +13171,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -13193,8 +13193,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "eighteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -13204,8 +13204,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 24,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-four (24)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -13358,7 +13358,7 @@
"dictionaryPriority": 0,
"hasReading": false,
"frequency": 7,
- "displayValue": null,
+ "displayValue": "seven",
"displayValueParsed": false
},
{
@@ -13380,8 +13380,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "nineteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -13391,8 +13391,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 25,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-five (25)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -13402,7 +13402,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 31,
- "displayValue": null,
+ "displayValue": "thirty-one",
"displayValueParsed": false
}
]
@@ -13881,8 +13881,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "sixteen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -13892,8 +13892,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 22,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-two (22)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -14048,8 +14048,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 0,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "seventeen",
+ "displayValueParsed": true
},
{
"index": 4,
@@ -14059,8 +14059,8 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 23,
- "displayValue": null,
- "displayValueParsed": false
+ "displayValue": "twenty-three (23)",
+ "displayValueParsed": true
},
{
"index": 5,
@@ -14070,7 +14070,7 @@
"dictionaryPriority": 0,
"hasReading": true,
"frequency": 29,
- "displayValue": null,
+ "displayValue": "twenty-nine",
"displayValueParsed": false
}
]
diff --git a/test/dictionary.test.js b/test/dictionary.test.js
index 8f160bc1..e516bd8e 100644
--- a/test/dictionary.test.js
+++ b/test/dictionary.test.js
@@ -24,12 +24,18 @@ import {createDictionaryArchive} from '../dev/util.js';
const dirname = path.dirname(fileURLToPath(import.meta.url));
+/**
+ * @param {string} dictionary
+ * @param {string} [dictionaryName]
+ * @returns {import('jszip')}
+ */
function createTestDictionaryArchive(dictionary, dictionaryName) {
const dictionaryDirectory = path.join(dirname, 'data', 'dictionaries', dictionary);
return createDictionaryArchive(dictionaryDirectory, dictionaryName);
}
+/** */
async function main() {
const dictionaries = [
{name: 'valid-dictionary1', valid: true},
diff --git a/test/jsconfig.json b/test/jsconfig.json
new file mode 100644
index 00000000..b025918c
--- /dev/null
+++ b/test/jsconfig.json
@@ -0,0 +1,39 @@
+{
+ "compilerOptions": {
+ "module": "ES2015",
+ "target": "ES2022",
+ "checkJs": true,
+ "moduleResolution": "node",
+ "strict": true,
+ "strictNullChecks": true,
+ "noImplicitAny": true,
+ "strictPropertyInitialization": true,
+ "suppressImplicitAnyIndexErrors": false,
+ "skipLibCheck": false,
+ "baseUrl": ".",
+ "paths": {
+ "*": ["../types/ext/*"],
+ "dev/*": ["../types/dev/*"],
+ "test/*": ["../types/test/*"]
+ },
+ "types": [
+ "chrome",
+ "firefox-webext-browser",
+ "handlebars",
+ "jszip",
+ "parse5",
+ "wanakana"
+ ]
+ },
+ "include": [
+ "**/*.js",
+ "../ext/**/*.js",
+ "../types/ext/**/*.ts",
+ "../types/dev/**/*.ts",
+ "../types/other/web-set-timeout.d.ts"
+ ],
+ "exclude": [
+ "../node_modules",
+ "../ext/lib"
+ ]
+} \ No newline at end of file
diff --git a/test/playwright/visual.spec.js b/test/playwright/visual.spec.js
index 2f46990f..8b48b7c0 100644
--- a/test/playwright/visual.spec.js
+++ b/test/playwright/visual.spec.js
@@ -48,10 +48,16 @@ test('visual', async ({page, extensionId}) => {
// take a screenshot of the settings page with jmdict loaded
await expect.soft(page).toHaveScreenshot('settings-jmdict-loaded.png', {mask: [storage_locator]});
+ /**
+ * @param {number} doc_number
+ * @param {number} test_number
+ * @param {import('@playwright/test').ElementHandle<Node>} el
+ * @param {{x: number, y: number}} offset
+ */
const screenshot = async (doc_number, test_number, el, offset) => {
const test_name = 'doc' + doc_number + '-test' + test_number;
- const box = await el.boundingBox();
+ const box = (await el.boundingBox()) || {x: 0, y: 0, width: 0, height: 0};
// find the popup frame if it exists
let popup_frame = page.frames().find((f) => f.url().includes('popup.html'));
@@ -66,7 +72,7 @@ test('visual', async ({page, extensionId}) => {
popup_frame = await frame_attached; // wait for popup to be attached
}
try {
- await (await popup_frame.frameElement()).waitForElementState('visible', {timeout: 500}); // some tests don't have a popup, so don't fail if it's not there; TODO: check if the popup is expected to be there
+ await (await /** @type {import('@playwright/test').Frame} */ (popup_frame).frameElement()).waitForElementState('visible', {timeout: 500}); // some tests don't have a popup, so don't fail if it's not there; TODO: check if the popup is expected to be there
} catch (error) {
console.log(test_name + ' has no popup');
}
@@ -75,7 +81,7 @@ test('visual', async ({page, extensionId}) => {
await expect.soft(page).toHaveScreenshot(test_name + '.png');
await page.mouse.click(0, 0); // click away so popup disappears
- await (await popup_frame.frameElement()).waitForElementState('hidden'); // wait for popup to disappear
+ await (await /** @type {import('@playwright/test').Frame} */ (popup_frame).frameElement()).waitForElementState('hidden'); // wait for popup to disappear
};
// Load test-document1.html
diff --git a/test/test-all.js b/test/test-all.js
new file mode 100644
index 00000000..9219d278
--- /dev/null
+++ b/test/test-all.js
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 fs = require('fs');
+const path = require('path');
+const {spawnSync} = require('child_process');
+const {getArgs} = require('../dev/util');
+
+
+/**
+ * @throws {Error}
+ */
+function main() {
+ const args = getArgs(process.argv.slice(2), new Map(/** @type {[key: string, value: (boolean|null|number|string|string[])][]} */ ([
+ ['skip', []],
+ [null, []]
+ ])));
+ const directories = /** @type {string[]} */ (args.get(null));
+ const skipArg = /** @type {string[]} */ (args.get('skip'));
+ const skip = new Set([__filename, ...skipArg].map((value) => path.resolve(value)));
+
+ const node = process.execPath;
+ const fileNamePattern = /\.js$/i;
+
+ let first = true;
+ for (const directory of directories) {
+ const fileNames = fs.readdirSync(directory);
+ for (const fileName of fileNames) {
+ if (!fileNamePattern.test(fileName)) { continue; }
+
+ const fullFileName = path.resolve(path.join(directory, fileName));
+ if (skip.has(fullFileName)) { continue; }
+
+ const stats = fs.lstatSync(fullFileName);
+ if (!stats.isFile()) { continue; }
+
+ process.stdout.write(`${first ? '' : '\n'}Running ${fileName}...\n`);
+ first = false;
+
+ const {error, status} = spawnSync(node, [fileName], {cwd: directory, stdio: 'inherit'});
+
+ if (status !== null && status !== 0) {
+ process.exit(status);
+ return;
+ }
+ if (error) {
+ throw error;
+ }
+ }
+ }
+
+ process.exit(0);
+}
+
+
+if (require.main === module) { main(); }
diff --git a/test/test-anki-note-builder.js b/test/test-anki-note-builder.js
new file mode 100644
index 00000000..8e0ab9d9
--- /dev/null
+++ b/test/test-anki-note-builder.js
@@ -0,0 +1,322 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2021-2022 Yomichan Authors
+ *
+ * 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 fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+const {JSDOM} = require('jsdom');
+const {testMain} = require('../dev/util');
+const {TranslatorVM} = require('../dev/translator-vm');
+
+
+/**
+ * @template T
+ * @param {T} value
+ * @returns {T}
+ */
+function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+/**
+ * @returns {Promise<{vm: TranslatorVM, AnkiNoteBuilder: typeof AnkiNoteBuilder2, JapaneseUtil: typeof JapaneseUtil2}>}
+ */
+async function createVM() {
+ const dom = new JSDOM();
+ const {Node, NodeFilter, document} = dom.window;
+
+ const vm = new TranslatorVM({
+ Node,
+ NodeFilter,
+ document,
+ location: new URL('https://yomichan.test/')
+ });
+
+ const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1');
+ await vm.prepare(dictionaryDirectory, 'Test Dictionary 2');
+
+ vm.execute([
+ 'js/data/anki-note-builder.js',
+ 'js/data/anki-util.js',
+ 'js/dom/sandbox/css-style-applier.js',
+ 'js/display/sandbox/pronunciation-generator.js',
+ 'js/display/sandbox/structured-content-generator.js',
+ 'js/templates/sandbox/anki-template-renderer.js',
+ 'js/templates/sandbox/anki-template-renderer-content-manager.js',
+ 'js/templates/sandbox/template-renderer.js',
+ 'js/templates/sandbox/template-renderer-media-provider.js',
+ 'lib/handlebars.min.js'
+ ]);
+
+ /** @type {[typeof JapaneseUtil, typeof AnkiNoteBuilder, typeof AnkiTemplateRenderer]} */
+ const [
+ JapaneseUtil2,
+ AnkiNoteBuilder2,
+ AnkiTemplateRenderer2
+ ] = vm.get([
+ 'JapaneseUtil',
+ 'AnkiNoteBuilder',
+ 'AnkiTemplateRenderer'
+ ]);
+
+ class TemplateRendererProxy {
+ constructor() {
+ /** @type {?Promise<void>} */
+ this._preparePromise = null;
+ /** @type {AnkiTemplateRenderer} */
+ this._ankiTemplateRenderer = new AnkiTemplateRenderer2();
+ }
+
+ /**
+ * @param {string} template
+ * @param {import('template-renderer').PartialOrCompositeRenderData} data
+ * @param {import('anki-templates').RenderMode} type
+ * @returns {Promise<import('template-renderer').RenderResult>}
+ */
+ async render(template, data, type) {
+ await this._prepare();
+ return await this._ankiTemplateRenderer.templateRenderer.render(template, data, type);
+ }
+
+ /**
+ * @param {import('template-renderer').RenderMultiItem[]} items
+ * @returns {Promise<import('core').Response<import('template-renderer').RenderResult>[]>}
+ */
+ async renderMulti(items) {
+ await this._prepare();
+ return await this._ankiTemplateRenderer.templateRenderer.renderMulti(items);
+ }
+
+ /**
+ * @returns {Promise<void>}
+ */
+ _prepare() {
+ if (this._preparePromise === null) {
+ this._preparePromise = this._prepareInternal();
+ }
+ return this._preparePromise;
+ }
+
+ /** */
+ async _prepareInternal() {
+ await this._ankiTemplateRenderer.prepare();
+ }
+ }
+ vm.set({TemplateRendererProxy});
+
+ return {vm, AnkiNoteBuilder: AnkiNoteBuilder2, JapaneseUtil: JapaneseUtil2};
+}
+
+/**
+ * @param {'terms'|'kanji'} type
+ * @returns {string[]}
+ */
+function getFieldMarkers(type) {
+ switch (type) {
+ case 'terms':
+ return [
+ 'audio',
+ 'clipboard-image',
+ 'clipboard-text',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'conjugation',
+ 'dictionary',
+ 'document-title',
+ 'expression',
+ 'frequencies',
+ 'furigana',
+ 'furigana-plain',
+ 'glossary',
+ 'glossary-brief',
+ 'glossary-no-dictionary',
+ 'part-of-speech',
+ 'pitch-accents',
+ 'pitch-accent-graphs',
+ 'pitch-accent-positions',
+ 'reading',
+ 'screenshot',
+ 'search-query',
+ 'selection-text',
+ 'sentence',
+ 'sentence-furigana',
+ 'tags',
+ 'url'
+ ];
+ case 'kanji':
+ return [
+ 'character',
+ 'clipboard-image',
+ 'clipboard-text',
+ 'cloze-body',
+ 'cloze-prefix',
+ 'cloze-suffix',
+ 'dictionary',
+ 'document-title',
+ 'glossary',
+ 'kunyomi',
+ 'onyomi',
+ 'screenshot',
+ 'search-query',
+ 'selection-text',
+ 'sentence',
+ 'sentence-furigana',
+ 'stroke-count',
+ 'tags',
+ 'url'
+ ];
+ default:
+ return [];
+ }
+}
+
+/**
+ * @param {import('dictionary').DictionaryEntry[]} dictionaryEntries
+ * @param {'terms'|'kanji'} type
+ * @param {import('settings').ResultOutputMode} mode
+ * @param {string} template
+ * @param {typeof AnkiNoteBuilder} AnkiNoteBuilder
+ * @param {typeof JapaneseUtil} JapaneseUtil
+ * @param {boolean} write
+ * @returns {Promise<import('anki').NoteFields[]>}
+ */
+async function getRenderResults(dictionaryEntries, type, mode, template, AnkiNoteBuilder, JapaneseUtil, write) {
+ const markers = getFieldMarkers(type);
+ /** @type {import('anki-note-builder').Field[]} */
+ const fields = [];
+ for (const marker of markers) {
+ fields.push([marker, `{${marker}}`]);
+ }
+
+ const japaneseUtil = new JapaneseUtil(null);
+ const clozePrefix = 'cloze-prefix';
+ const clozeSuffix = 'cloze-suffix';
+ const results = [];
+ for (const dictionaryEntry of dictionaryEntries) {
+ let source = '';
+ switch (dictionaryEntry.type) {
+ case 'kanji':
+ source = dictionaryEntry.character;
+ break;
+ case 'term':
+ if (dictionaryEntry.headwords.length > 0 && dictionaryEntry.headwords[0].sources.length > 0) {
+ source = dictionaryEntry.headwords[0].sources[0].originalText;
+ }
+ break;
+ }
+ const ankiNoteBuilder = new AnkiNoteBuilder({japaneseUtil});
+ const context = {
+ url: 'url:',
+ sentence: {
+ text: `${clozePrefix}${source}${clozeSuffix}`,
+ offset: clozePrefix.length
+ },
+ documentTitle: 'title',
+ query: 'query',
+ fullQuery: 'fullQuery'
+ };
+ /** @type {import('anki-note-builder').CreateNoteDetails} */
+ const details = {
+ dictionaryEntry,
+ mode: 'test',
+ context,
+ template,
+ deckName: 'deckName',
+ modelName: 'modelName',
+ fields,
+ tags: ['yomichan'],
+ checkForDuplicates: true,
+ duplicateScope: 'collection',
+ duplicateScopeCheckAllModels: false,
+ resultOutputMode: mode,
+ glossaryLayoutMode: 'default',
+ compactTags: false,
+ requirements: [],
+ mediaOptions: null
+ };
+ const {note: {fields: noteFields}, errors} = await ankiNoteBuilder.createNote(details);
+ if (!write) {
+ for (const error of errors) {
+ console.error(error);
+ }
+ assert.strictEqual(errors.length, 0);
+ }
+ results.push(noteFields);
+ }
+
+ return results;
+}
+
+
+/** */
+async function main() {
+ const write = (process.argv[2] === '--write');
+
+ const {vm, AnkiNoteBuilder, JapaneseUtil} = await createVM();
+
+ const testInputsFilePath = path.join(__dirname, 'data', 'translator-test-inputs.json');
+ const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'}));
+
+ const testResults1FilePath = path.join(__dirname, 'data', 'anki-note-builder-test-results.json');
+ const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'}));
+ const actualResults1 = [];
+
+ const template = fs.readFileSync(path.join(__dirname, '..', 'ext', 'data/templates/default-anki-field-templates.handlebars'), {encoding: 'utf8'});
+
+ for (let i = 0, ii = tests.length; i < ii; ++i) {
+ const test = tests[i];
+ const expected1 = expectedResults1[i];
+ switch (test.func) {
+ case 'findTerms':
+ {
+ const {name, mode, text} = test;
+ /** @type {import('translation').FindTermsOptions} */
+ const options = vm.buildOptions(optionsPresets, test.options);
+ const {dictionaryEntries} = clone(await vm.translator.findTerms(mode, text, options));
+ const results = mode !== 'simple' ? clone(await getRenderResults(dictionaryEntries, 'terms', mode, template, AnkiNoteBuilder, JapaneseUtil, write)) : null;
+ actualResults1.push({name, results});
+ if (!write) {
+ assert.deepStrictEqual(results, expected1.results);
+ }
+ }
+ break;
+ case 'findKanji':
+ {
+ const {name, text} = test;
+ /** @type {import('translation').FindKanjiOptions} */
+ const options = vm.buildOptions(optionsPresets, test.options);
+ const dictionaryEntries = clone(await vm.translator.findKanji(text, options));
+ const results = clone(await getRenderResults(dictionaryEntries, 'kanji', 'split', template, AnkiNoteBuilder, JapaneseUtil, write));
+ actualResults1.push({name, results});
+ if (!write) {
+ assert.deepStrictEqual(results, expected1.results);
+ }
+ }
+ break;
+ }
+ }
+
+ if (write) {
+ // Use 2 indent instead of 4 to save a bit of file size
+ fs.writeFileSync(testResults1FilePath, JSON.stringify(actualResults1, null, 2), {encoding: 'utf8'});
+ }
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-cache-map.js b/test/test-cache-map.js
new file mode 100644
index 00000000..56a31898
--- /dev/null
+++ b/test/test-cache-map.js
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+const vm = new VM({console});
+vm.execute([
+ 'js/general/cache-map.js'
+]);
+/** @type {typeof CacheMap} */
+const CacheMap2 = vm.getSingle('CacheMap');
+
+
+/** */
+function testConstructor() {
+ const data = /** @type {[throws: boolean, create: () => void][]} */ ([
+ [false, () => new CacheMap2(0)],
+ [false, () => new CacheMap2(1)],
+ [false, () => new CacheMap2(Number.MAX_VALUE)],
+ [true, () => new CacheMap2(-1)],
+ [true, () => new CacheMap2(1.5)],
+ [true, () => new CacheMap2(Number.NaN)],
+ [true, () => new CacheMap2(Number.POSITIVE_INFINITY)],
+ // @ts-ignore - Ignore because it should throw an error
+ [true, () => new CacheMap2('a')]
+ ]);
+
+ for (const [throws, create] of data) {
+ if (throws) {
+ assert.throws(create);
+ } else {
+ assert.doesNotThrow(create);
+ }
+ }
+}
+
+/** */
+function testApi() {
+ const data = [
+ {
+ maxSize: 1,
+ expectedSize: 0,
+ calls: []
+ },
+ {
+ maxSize: 10,
+ expectedSize: 1,
+ calls: [
+ {func: 'get', args: ['a1-b-c'], returnValue: void 0},
+ {func: 'has', args: ['a1-b-c'], returnValue: false},
+ {func: 'set', args: ['a1-b-c', 32], returnValue: void 0},
+ {func: 'get', args: ['a1-b-c'], returnValue: 32},
+ {func: 'has', args: ['a1-b-c'], returnValue: true}
+ ]
+ },
+ {
+ maxSize: 10,
+ expectedSize: 2,
+ calls: [
+ {func: 'set', args: ['a1-b-c', 32], returnValue: void 0},
+ {func: 'get', args: ['a1-b-c'], returnValue: 32},
+ {func: 'set', args: ['a1-b-c', 64], returnValue: void 0},
+ {func: 'get', args: ['a1-b-c'], returnValue: 64},
+ {func: 'set', args: ['a2-b-c', 96], returnValue: void 0},
+ {func: 'get', args: ['a2-b-c'], returnValue: 96}
+ ]
+ },
+ {
+ maxSize: 2,
+ expectedSize: 2,
+ calls: [
+ {func: 'has', args: ['a1-b-c'], returnValue: false},
+ {func: 'has', args: ['a2-b-c'], returnValue: false},
+ {func: 'has', args: ['a3-b-c'], returnValue: false},
+ {func: 'set', args: ['a1-b-c', 1], returnValue: void 0},
+ {func: 'has', args: ['a1-b-c'], returnValue: true},
+ {func: 'has', args: ['a2-b-c'], returnValue: false},
+ {func: 'has', args: ['a3-b-c'], returnValue: false},
+ {func: 'set', args: ['a2-b-c', 2], returnValue: void 0},
+ {func: 'has', args: ['a1-b-c'], returnValue: true},
+ {func: 'has', args: ['a2-b-c'], returnValue: true},
+ {func: 'has', args: ['a3-b-c'], returnValue: false},
+ {func: 'set', args: ['a3-b-c', 3], returnValue: void 0},
+ {func: 'has', args: ['a1-b-c'], returnValue: false},
+ {func: 'has', args: ['a2-b-c'], returnValue: true},
+ {func: 'has', args: ['a3-b-c'], returnValue: true}
+ ]
+ }
+ ];
+
+ for (const {maxSize, expectedSize, calls} of data) {
+ const cache = new CacheMap2(maxSize);
+ assert.strictEqual(cache.maxSize, maxSize);
+ for (const call of calls) {
+ const {func, args} = call;
+ let returnValue;
+ switch (func) {
+ case 'get': returnValue = cache.get(args[0]); break;
+ case 'set': returnValue = cache.set(args[0], args[1]); break;
+ case 'has': returnValue = cache.has(args[0]); break;
+ case 'clear': returnValue = cache.clear(); break;
+ }
+ if (Object.prototype.hasOwnProperty.call(call, 'returnValue')) {
+ const {returnValue: expectedReturnValue} = call;
+ assert.deepStrictEqual(returnValue, expectedReturnValue);
+ }
+ }
+ assert.strictEqual(cache.size, expectedSize);
+ }
+}
+
+
+/** */
+function main() {
+ testConstructor();
+ testApi();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-core.js b/test/test-core.js
new file mode 100644
index 00000000..2c48ac20
--- /dev/null
+++ b/test/test-core.js
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+const vm = new VM();
+vm.execute([
+ 'js/core.js',
+ 'js/core/extension-error.js'
+]);
+const values = vm.get(['DynamicProperty', 'deepEqual']);
+const DynamicProperty2 = /** @type {typeof DynamicProperty} */ (values[0]);
+const deepEqual2 = /** @type {typeof deepEqual} */ (values[1]);
+
+
+/** */
+function testDynamicProperty() {
+ const data = [
+ {
+ initialValue: 0,
+ /** @type {{operation: ?string, expectedDefaultValue: number, expectedValue: number, expectedOverrideCount: number, expeectedEventOccurred: boolean, args: [value: number, priority?: number]}[]} */
+ operations: [
+ {
+ operation: null,
+ args: [0],
+ expectedDefaultValue: 0,
+ expectedValue: 0,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'set.defaultValue',
+ args: [1],
+ expectedDefaultValue: 1,
+ expectedValue: 1,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'set.defaultValue',
+ args: [1],
+ expectedDefaultValue: 1,
+ expectedValue: 1,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'set.defaultValue',
+ args: [0],
+ expectedDefaultValue: 0,
+ expectedValue: 0,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'setOverride',
+ args: [8],
+ expectedDefaultValue: 0,
+ expectedValue: 8,
+ expectedOverrideCount: 1,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'setOverride',
+ args: [16],
+ expectedDefaultValue: 0,
+ expectedValue: 8,
+ expectedOverrideCount: 2,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'setOverride',
+ args: [32, 1],
+ expectedDefaultValue: 0,
+ expectedValue: 32,
+ expectedOverrideCount: 3,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'setOverride',
+ args: [64, -1],
+ expectedDefaultValue: 0,
+ expectedValue: 32,
+ expectedOverrideCount: 4,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'clearOverride',
+ args: [-4],
+ expectedDefaultValue: 0,
+ expectedValue: 32,
+ expectedOverrideCount: 3,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'clearOverride',
+ args: [-3],
+ expectedDefaultValue: 0,
+ expectedValue: 32,
+ expectedOverrideCount: 2,
+ expeectedEventOccurred: false
+ },
+ {
+ operation: 'clearOverride',
+ args: [-2],
+ expectedDefaultValue: 0,
+ expectedValue: 64,
+ expectedOverrideCount: 1,
+ expeectedEventOccurred: true
+ },
+ {
+ operation: 'clearOverride',
+ args: [-1],
+ expectedDefaultValue: 0,
+ expectedValue: 0,
+ expectedOverrideCount: 0,
+ expeectedEventOccurred: true
+ }
+ ]
+ }
+ ];
+
+ for (const {initialValue, operations} of data) {
+ const property = new DynamicProperty2(initialValue);
+ const overrideTokens = [];
+ let eventOccurred = false;
+ const onChange = () => { eventOccurred = true; };
+ property.on('change', onChange);
+ for (const {operation, args, expectedDefaultValue, expectedValue, expectedOverrideCount, expeectedEventOccurred} of operations) {
+ eventOccurred = false;
+ switch (operation) {
+ case 'set.defaultValue': property.defaultValue = args[0]; break;
+ case 'setOverride': overrideTokens.push(property.setOverride(...args)); break;
+ case 'clearOverride': property.clearOverride(overrideTokens[overrideTokens.length + args[0]]); break;
+ }
+ assert.strictEqual(eventOccurred, expeectedEventOccurred);
+ assert.strictEqual(property.defaultValue, expectedDefaultValue);
+ assert.strictEqual(property.value, expectedValue);
+ assert.strictEqual(property.overrideCount, expectedOverrideCount);
+ }
+ property.off('change', onChange);
+ }
+}
+
+/** */
+function testDeepEqual() {
+ const data = [
+ // Simple tests
+ {
+ value1: 0,
+ value2: 0,
+ expected: true
+ },
+ {
+ value1: null,
+ value2: null,
+ expected: true
+ },
+ {
+ value1: 'test',
+ value2: 'test',
+ expected: true
+ },
+ {
+ value1: true,
+ value2: true,
+ expected: true
+ },
+ {
+ value1: 0,
+ value2: 1,
+ expected: false
+ },
+ {
+ value1: null,
+ value2: false,
+ expected: false
+ },
+ {
+ value1: 'test1',
+ value2: 'test2',
+ expected: false
+ },
+ {
+ value1: true,
+ value2: false,
+ expected: false
+ },
+
+ // Simple object tests
+ {
+ value1: {},
+ value2: {},
+ expected: true
+ },
+ {
+ value1: {},
+ value2: [],
+ expected: false
+ },
+ {
+ value1: [],
+ value2: [],
+ expected: true
+ },
+ {
+ value1: {},
+ value2: null,
+ expected: false
+ },
+
+ // Complex object tests
+ {
+ value1: [1],
+ value2: [],
+ expected: false
+ },
+ {
+ value1: [1],
+ value2: [1],
+ expected: true
+ },
+ {
+ value1: [1],
+ value2: [2],
+ expected: false
+ },
+
+ {
+ value1: {},
+ value2: {test: 1},
+ expected: false
+ },
+ {
+ value1: {test: 1},
+ value2: {test: 1},
+ expected: true
+ },
+ {
+ value1: {test: 1},
+ value2: {test: {test2: false}},
+ expected: false
+ },
+ {
+ value1: {test: {test2: true}},
+ value2: {test: {test2: false}},
+ expected: false
+ },
+ {
+ value1: {test: {test2: [true]}},
+ value2: {test: {test2: [true]}},
+ expected: true
+ },
+
+ // Recursive
+ {
+ value1: (() => { const x = {}; x.x = x; return x; })(),
+ value2: (() => { const x = {}; x.x = x; return x; })(),
+ expected: false
+ }
+ ];
+
+ let index = 0;
+ for (const {value1, value2, expected} of data) {
+ const actual1 = deepEqual2(value1, value2);
+ assert.strictEqual(actual1, expected, `Failed for test ${index}`);
+
+ const actual2 = deepEqual2(value2, value1);
+ assert.strictEqual(actual2, expected, `Failed for test ${index}`);
+
+ ++index;
+ }
+}
+
+
+/** */
+function main() {
+ testDynamicProperty();
+ testDeepEqual();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-database.js b/test/test-database.js
new file mode 100644
index 00000000..947e369b
--- /dev/null
+++ b/test/test-database.js
@@ -0,0 +1,982 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 path = require('path');
+const assert = require('assert');
+const {createDictionaryArchive, testMain} = require('../dev/util');
+const {DatabaseVM, DatabaseVMDictionaryImporterMediaLoader} = require('../dev/database-vm');
+
+
+const vm = new DatabaseVM();
+vm.execute([
+ 'js/core.js',
+ 'js/core/extension-error.js',
+ 'js/general/cache-map.js',
+ 'js/data/json-schema.js',
+ 'js/media/media-util.js',
+ 'js/language/dictionary-importer.js',
+ 'js/data/database.js',
+ 'js/language/dictionary-database.js'
+]);
+/** @type {typeof DictionaryImporter} */
+const DictionaryImporter2 = vm.getSingle('DictionaryImporter');
+/** @type {typeof DictionaryDatabase} */
+const DictionaryDatabase2 = vm.getSingle('DictionaryDatabase');
+
+
+/**
+ * @param {string} dictionary
+ * @param {string} [dictionaryName]
+ * @returns {import('jszip')}
+ */
+function createTestDictionaryArchive(dictionary, dictionaryName) {
+ const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', dictionary);
+ return createDictionaryArchive(dictionaryDirectory, dictionaryName);
+}
+
+
+/**
+ * @param {import('dictionary-importer').OnProgressCallback} [onProgress]
+ * @returns {DictionaryImporter}
+ */
+function createDictionaryImporter(onProgress) {
+ const dictionaryImporterMediaLoader = new DatabaseVMDictionaryImporterMediaLoader();
+ return new DictionaryImporter2(dictionaryImporterMediaLoader, (...args) => {
+ const {stepIndex, stepCount, index, count} = args[0];
+ assert.ok(stepIndex < stepCount);
+ assert.ok(index <= count);
+ if (typeof onProgress === 'function') {
+ onProgress(...args);
+ }
+ });
+}
+
+
+/**
+ * @param {import('dictionary-database').TermEntry[]} dictionaryDatabaseEntries
+ * @param {string} term
+ * @returns {number}
+ */
+function countDictionaryDatabaseEntriesWithTerm(dictionaryDatabaseEntries, term) {
+ return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.term === term ? 1 : 0)), 0);
+}
+
+/**
+ * @param {import('dictionary-database').TermEntry[]} dictionaryDatabaseEntries
+ * @param {string} reading
+ * @returns {number}
+ */
+function countDictionaryDatabaseEntriesWithReading(dictionaryDatabaseEntries, reading) {
+ return dictionaryDatabaseEntries.reduce((i, v) => (i + (v.reading === reading ? 1 : 0)), 0);
+}
+
+/**
+ * @param {import('dictionary-database').TermMeta[]|import('dictionary-database').KanjiMeta[]} metas
+ * @param {import('dictionary-database').TermMetaType|import('dictionary-database').KanjiMetaType} mode
+ * @returns {number}
+ */
+function countMetasWithMode(metas, mode) {
+ let i = 0;
+ for (const item of metas) {
+ if (item.mode === mode) { ++i; }
+ }
+ return i;
+}
+
+/**
+ * @param {import('dictionary-database').KanjiEntry[]} kanji
+ * @param {string} character
+ * @returns {number}
+ */
+function countKanjiWithCharacter(kanji, character) {
+ let i = 0;
+ for (const item of kanji) {
+ if (item.character === character) { ++i; }
+ }
+ return i;
+}
+
+
+/**
+ * @param {number} timeout
+ * @returns {Promise<void>}
+ */
+function clearDatabase(timeout) {
+ return new Promise((resolve, reject) => {
+ /** @type {?number} */
+ let timer = setTimeout(() => {
+ timer = null;
+ reject(new Error(`clearDatabase failed to resolve after ${timeout}ms`));
+ }, timeout);
+
+ (async () => {
+ const indexedDB = vm.indexedDB;
+ for (const {name} of await indexedDB.databases()) {
+ if (typeof name !== 'string') { continue; }
+ /** @type {Promise<void>} */
+ const promise2 = new Promise((resolve2, reject2) => {
+ const request = indexedDB.deleteDatabase(name);
+ request.onerror = (e) => reject2(e);
+ request.onsuccess = () => resolve2();
+ });
+ await promise2;
+ }
+ if (timer !== null) {
+ clearTimeout(timer);
+ }
+ resolve();
+ })();
+ });
+}
+
+
+/** */
+async function testDatabase1() {
+ // Load dictionary data
+ const testDictionary = createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+ const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
+
+ const title = testDictionaryIndex.title;
+ const titles = new Map([
+ [title, {priority: 0, allowSecondarySearches: false}]
+ ]);
+
+ // Setup iteration data
+ const iterations = [
+ {
+ cleanup: async () => {
+ // Test purge
+ await dictionaryDatabase.purge();
+ await testDatabaseEmpty1(dictionaryDatabase);
+ }
+ },
+ {
+ cleanup: async () => {
+ // Test deleteDictionary
+ let progressEvent = false;
+ await dictionaryDatabase.deleteDictionary(
+ title,
+ 1000,
+ () => {
+ progressEvent = true;
+ }
+ );
+ assert.ok(progressEvent);
+
+ await testDatabaseEmpty1(dictionaryDatabase);
+ }
+ },
+ {
+ cleanup: async () => {}
+ }
+ ];
+
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase2();
+ await dictionaryDatabase.prepare();
+
+ for (const {cleanup} of iterations) {
+ const expectedSummary = {
+ title,
+ revision: 'test',
+ sequenced: true,
+ version: 3,
+ importDate: 0,
+ prefixWildcardsSupported: true,
+ counts: {
+ kanji: {total: 2},
+ kanjiMeta: {total: 6, freq: 6},
+ media: {total: 4},
+ tagMeta: {total: 15},
+ termMeta: {total: 38, freq: 31, pitch: 7},
+ terms: {total: 21}
+ }
+ };
+
+ // Import data
+ let progressEvent = false;
+ const dictionaryImporter = createDictionaryImporter(() => { progressEvent = true; });
+ const {result, errors} = await dictionaryImporter.importDictionary(
+ dictionaryDatabase,
+ testDictionarySource,
+ {prefixWildcardsSupported: true}
+ );
+ expectedSummary.importDate = result.importDate;
+ vm.assert.deepStrictEqual(errors, []);
+ vm.assert.deepStrictEqual(result, expectedSummary);
+ assert.ok(progressEvent);
+
+ // Get info summary
+ const info = await dictionaryDatabase.getDictionaryInfo();
+ vm.assert.deepStrictEqual(info, [expectedSummary]);
+
+ // Get counts
+ const counts = await dictionaryDatabase.getDictionaryCounts(
+ info.map((v) => v.title),
+ true
+ );
+ vm.assert.deepStrictEqual(counts, {
+ counts: [{kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}],
+ total: {kanji: 2, kanjiMeta: 6, terms: 21, termMeta: 38, tagMeta: 15, media: 4}
+ });
+
+ // Test find* functions
+ await testFindTermsBulkTest1(dictionaryDatabase, titles);
+ await testTindTermsExactBulk1(dictionaryDatabase, titles);
+ await testFindTermsBySequenceBulk1(dictionaryDatabase, title);
+ await testFindTermMetaBulk1(dictionaryDatabase, titles);
+ await testFindKanjiBulk1(dictionaryDatabase, titles);
+ await testFindKanjiMetaBulk1(dictionaryDatabase, titles);
+ await testFindTagForTitle1(dictionaryDatabase, title);
+
+ // Cleanup
+ await cleanup();
+ }
+
+ await dictionaryDatabase.close();
+}
+
+/**
+ * @param {DictionaryDatabase} database
+ */
+async function testDatabaseEmpty1(database) {
+ const info = await database.getDictionaryInfo();
+ vm.assert.deepStrictEqual(info, []);
+
+ const counts = await database.getDictionaryCounts([], true);
+ vm.assert.deepStrictEqual(counts, {
+ counts: [],
+ total: {kanji: 0, kanjiMeta: 0, terms: 0, termMeta: 0, tagMeta: 0, media: 0}
+ });
+}
+
+/**
+ * @param {DictionaryDatabase} database
+ * @param {import('dictionary-database').DictionarySet} titles
+ */
+async function testFindTermsBulkTest1(database, titles) {
+ /** @type {{inputs: {matchType: import('dictionary-database').MatchType, termList: string[]}[], expectedResults: {total: number, terms: [key: string, count: number][], readings: [key: string, count: number][]}}[]} */
+ const data = [
+ {
+ inputs: [
+ {
+ matchType: 'exact',
+ termList: ['打', '打つ', '打ち込む']
+ },
+ {
+ matchType: 'exact',
+ termList: ['だ', 'ダース', 'うつ', 'ぶつ', 'うちこむ', 'ぶちこむ']
+ },
+ {
+ matchType: 'prefix',
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 10,
+ terms: [
+ ['打', 2],
+ ['打つ', 4],
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 2],
+ ['ぶつ', 2],
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: 'exact',
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: 'suffix',
+ termList: ['込む']
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ matchType: 'exact',
+ termList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList, matchType} of inputs) {
+ const results = await database.findTermsBulk(termList, titles, matchType);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+/**
+ * @param {DictionaryDatabase} database
+ * @param {import('dictionary-database').DictionarySet} titles
+ */
+async function testTindTermsExactBulk1(database, titles) {
+ /** @type {{inputs: {termList: {term: string, reading: string}[]}[], expectedResults: {total: number, terms: [key: string, count: number][], readings: [key: string, count: number][]}}[]} */
+ const data = [
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打', reading: 'だ'},
+ {term: '打つ', reading: 'うつ'},
+ {term: '打ち込む', reading: 'うちこむ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 5,
+ terms: [
+ ['打', 1],
+ ['打つ', 2],
+ ['打ち込む', 2]
+ ],
+ readings: [
+ ['だ', 1],
+ ['うつ', 2],
+ ['うちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打', reading: 'だ?'},
+ {term: '打つ', reading: 'うつ?'},
+ {term: '打ち込む', reading: 'うちこむ?'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打つ', reading: 'うつ'},
+ {term: '打つ', reading: 'ぶつ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打つ', 4]
+ ],
+ readings: [
+ ['うつ', 2],
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: [
+ {term: '打つ', reading: 'うちこむ'}
+ ]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList} of inputs) {
+ const results = await database.findTermsExactBulk(termList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+/**
+ * @param {DictionaryDatabase} database
+ * @param {string} mainDictionary
+ */
+async function testFindTermsBySequenceBulk1(database, mainDictionary) {
+ /** @type {{inputs: {sequenceList: number[]}[], expectedResults: {total: number, terms: [key: string, count: number][], readings: [key: string, count: number][]}}[]} */
+ const data = [
+ {
+ inputs: [
+ {
+ sequenceList: [1, 2, 3, 4, 5]
+ }
+ ],
+ expectedResults: {
+ total: 11,
+ terms: [
+ ['打', 2],
+ ['打つ', 4],
+ ['打ち込む', 4],
+ ['画像', 1]
+ ],
+ readings: [
+ ['だ', 1],
+ ['ダース', 1],
+ ['うつ', 2],
+ ['ぶつ', 2],
+ ['うちこむ', 2],
+ ['ぶちこむ', 2],
+ ['がぞう', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [1]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['打', 1]
+ ],
+ readings: [
+ ['だ', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [2]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['打', 1]
+ ],
+ readings: [
+ ['ダース', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [3]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打つ', 4]
+ ],
+ readings: [
+ ['うつ', 2],
+ ['ぶつ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [4]
+ }
+ ],
+ expectedResults: {
+ total: 4,
+ terms: [
+ ['打ち込む', 4]
+ ],
+ readings: [
+ ['うちこむ', 2],
+ ['ぶちこむ', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [5]
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ terms: [
+ ['画像', 1]
+ ],
+ readings: [
+ ['がぞう', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: [-1]
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ },
+ {
+ inputs: [
+ {
+ sequenceList: []
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ terms: [],
+ readings: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {sequenceList} of inputs) {
+ const results = await database.findTermsBySequenceBulk(sequenceList.map((query) => ({query, dictionary: mainDictionary})));
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [term, count] of expectedResults.terms) {
+ assert.strictEqual(countDictionaryDatabaseEntriesWithTerm(results, term), count);
+ }
+ for (const [reading, count] of expectedResults.readings) {
+ assert.strictEqual(countDictionaryDatabaseEntriesWithReading(results, reading), count);
+ }
+ }
+ }
+}
+
+/**
+ * @param {DictionaryDatabase} database
+ * @param {import('dictionary-database').DictionarySet} titles
+ */
+async function testFindTermMetaBulk1(database, titles) {
+ /** @type {{inputs: {termList: string[]}[], expectedResults: {total: number, modes: [key: import('dictionary-database').TermMetaType, count: number][]}}[]} */
+ const data = [
+ {
+ inputs: [
+ {
+ termList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 11,
+ modes: [
+ ['freq', 11]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打つ']
+ }
+ ],
+ expectedResults: {
+ total: 10,
+ modes: [
+ ['freq', 10]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['打ち込む']
+ }
+ ],
+ expectedResults: {
+ total: 12,
+ modes: [
+ ['freq', 10],
+ ['pitch', 2]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ termList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {termList} of inputs) {
+ const results = await database.findTermMetaBulk(termList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ assert.strictEqual(countMetasWithMode(results, mode), count);
+ }
+ }
+ }
+}
+
+/**
+ * @param {DictionaryDatabase} database
+ * @param {import('dictionary-database').DictionarySet} titles
+ */
+async function testFindKanjiBulk1(database, titles) {
+ /** @type {{inputs: {kanjiList: string[]}[], expectedResults: {total: number, kanji: [key: string, count: number][]}}[]} */
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ kanji: [
+ ['打', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 1,
+ kanji: [
+ ['込', 1]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ kanji: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiBulk(kanjiList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [kanji, count] of expectedResults.kanji) {
+ assert.strictEqual(countKanjiWithCharacter(results, kanji), count);
+ }
+ }
+ }
+}
+
+/**
+ * @param {DictionaryDatabase} database
+ * @param {import('dictionary-database').DictionarySet} titles
+ */
+async function testFindKanjiMetaBulk1(database, titles) {
+ /** @type {{inputs: {kanjiList: string[]}[], expectedResults: {total: number, modes: [key: import('dictionary-database').KanjiMetaType, count: number][]}}[]} */
+ const data = [
+ {
+ inputs: [
+ {
+ kanjiList: ['打']
+ }
+ ],
+ expectedResults: {
+ total: 3,
+ modes: [
+ ['freq', 3]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['込']
+ }
+ ],
+ expectedResults: {
+ total: 3,
+ modes: [
+ ['freq', 3]
+ ]
+ }
+ },
+ {
+ inputs: [
+ {
+ kanjiList: ['?']
+ }
+ ],
+ expectedResults: {
+ total: 0,
+ modes: []
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {kanjiList} of inputs) {
+ const results = await database.findKanjiMetaBulk(kanjiList, titles);
+ assert.strictEqual(results.length, expectedResults.total);
+ for (const [mode, count] of expectedResults.modes) {
+ assert.strictEqual(countMetasWithMode(results, mode), count);
+ }
+ }
+ }
+}
+
+/**
+ * @param {DictionaryDatabase} database
+ * @param {string} title
+ */
+async function testFindTagForTitle1(database, title) {
+ const data = [
+ {
+ inputs: [
+ {
+ name: 'E1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'default', dictionary: title, name: 'E1', notes: 'example tag 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'K1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'default', dictionary: title, name: 'K1', notes: 'example kanji tag 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'kstat1'
+ }
+ ],
+ expectedResults: {
+ value: {category: 'class', dictionary: title, name: 'kstat1', notes: 'kanji stat 1', order: 0, score: 0}
+ }
+ },
+ {
+ inputs: [
+ {
+ name: 'invalid'
+ }
+ ],
+ expectedResults: {
+ value: null
+ }
+ }
+ ];
+
+ for (const {inputs, expectedResults} of data) {
+ for (const {name} of inputs) {
+ const result = await database.findTagForTitle(name, title);
+ vm.assert.deepStrictEqual(result, expectedResults.value);
+ }
+ }
+}
+
+
+/** */
+async function testDatabase2() {
+ // Load dictionary data
+ const testDictionary = createTestDictionaryArchive('valid-dictionary1');
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+ const testDictionaryIndex = JSON.parse(await testDictionary.files['index.json'].async('string'));
+
+ const title = testDictionaryIndex.title;
+ const titles = new Map([
+ [title, {priority: 0, allowSecondarySearches: false}]
+ ]);
+
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase2();
+ /** @type {import('dictionary-importer').ImportDetails} */
+ const detaultImportDetails = {prefixWildcardsSupported: false};
+
+ // Error: not prepared
+ await assert.rejects(async () => await dictionaryDatabase.deleteDictionary(title, 1000, () => {}));
+ await assert.rejects(async () => await dictionaryDatabase.findTermsBulk(['?'], titles, 'exact'));
+ await assert.rejects(async () => await dictionaryDatabase.findTermsExactBulk([{term: '?', reading: '?'}], titles));
+ await assert.rejects(async () => await dictionaryDatabase.findTermsBySequenceBulk([{query: 1, dictionary: title}]));
+ await assert.rejects(async () => await dictionaryDatabase.findTermMetaBulk(['?'], titles));
+ await assert.rejects(async () => await dictionaryDatabase.findTermMetaBulk(['?'], titles));
+ await assert.rejects(async () => await dictionaryDatabase.findKanjiBulk(['?'], titles));
+ await assert.rejects(async () => await dictionaryDatabase.findKanjiMetaBulk(['?'], titles));
+ await assert.rejects(async () => await dictionaryDatabase.findTagForTitle('tag', title));
+ await assert.rejects(async () => await dictionaryDatabase.getDictionaryInfo());
+ await assert.rejects(async () => await dictionaryDatabase.getDictionaryCounts([...titles.keys()], true));
+ await assert.rejects(async () => await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, detaultImportDetails));
+
+ await dictionaryDatabase.prepare();
+
+ // Error: already prepared
+ await assert.rejects(async () => await dictionaryDatabase.prepare());
+
+ await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, detaultImportDetails);
+
+ // Error: dictionary already imported
+ await assert.rejects(async () => await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, detaultImportDetails));
+
+ await dictionaryDatabase.close();
+}
+
+
+/** */
+async function testDatabase3() {
+ const invalidDictionaries = [
+ 'invalid-dictionary1',
+ 'invalid-dictionary2',
+ 'invalid-dictionary3',
+ 'invalid-dictionary4',
+ 'invalid-dictionary5',
+ 'invalid-dictionary6'
+ ];
+
+ // Setup database
+ const dictionaryDatabase = new DictionaryDatabase2();
+ /** @type {import('dictionary-importer').ImportDetails} */
+ const detaultImportDetails = {prefixWildcardsSupported: false};
+ await dictionaryDatabase.prepare();
+
+ for (const invalidDictionary of invalidDictionaries) {
+ const testDictionary = createTestDictionaryArchive(invalidDictionary);
+ const testDictionarySource = await testDictionary.generateAsync({type: 'arraybuffer'});
+
+ let error = null;
+ try {
+ await createDictionaryImporter().importDictionary(dictionaryDatabase, testDictionarySource, detaultImportDetails);
+ } catch (e) {
+ error = e;
+ }
+
+ if (error === null) {
+ assert.ok(false, `Expected an error while importing ${invalidDictionary}`);
+ } else {
+ const prefix = 'Dictionary has invalid data';
+ const message = /** @type {import('core').UnknownObject} */ (error).message;
+ assert.ok(typeof message, 'string');
+ if (typeof message === 'string') {
+ assert.ok(message.startsWith(prefix), `Expected error message to start with '${prefix}': ${message}`);
+ }
+ }
+ }
+
+ await dictionaryDatabase.close();
+}
+
+
+/** */
+async function main() {
+ const clearTimeout = 5000;
+ try {
+ await testDatabase1();
+ await clearDatabase(clearTimeout);
+
+ await testDatabase2();
+ await clearDatabase(clearTimeout);
+
+ await testDatabase3();
+ await clearDatabase(clearTimeout);
+ } catch (e) {
+ console.log(e);
+ process.exit(-1);
+ throw e;
+ }
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-document-util.js b/test/test-document-util.js
new file mode 100644
index 00000000..93ce1669
--- /dev/null
+++ b/test/test-document-util.js
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+const {JSDOM} = require('jsdom');
+const {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+
+// DOMRect class definition
+class DOMRect {
+ /**
+ * @param {number} x
+ * @param {number} y
+ * @param {number} width
+ * @param {number} height
+ */
+ constructor(x, y, width, height) {
+ /** @type {number} */
+ this._x = x;
+ /** @type {number} */
+ this._y = y;
+ /** @type {number} */
+ this._width = width;
+ /** @type {number} */
+ this._height = height;
+ }
+
+ /** @type {number} */
+ get x() { return this._x; }
+ /** @type {number} */
+ get y() { return this._y; }
+ /** @type {number} */
+ get width() { return this._width; }
+ /** @type {number} */
+ get height() { return this._height; }
+ /** @type {number} */
+ get left() { return this._x + Math.min(0, this._width); }
+ /** @type {number} */
+ get right() { return this._x + Math.max(0, this._width); }
+ /** @type {number} */
+ get top() { return this._y + Math.min(0, this._height); }
+ /** @type {number} */
+ get bottom() { return this._y + Math.max(0, this._height); }
+ /** @returns {string} */
+ toJSON() { return '<not implemented>'; }
+}
+
+
+/**
+ * @param {string} fileName
+ * @returns {JSDOM}
+ */
+function createJSDOM(fileName) {
+ const domSource = fs.readFileSync(fileName, {encoding: 'utf8'});
+ const dom = new JSDOM(domSource);
+ const document = dom.window.document;
+ const window = dom.window;
+
+ // Define innerText setter as an alias for textContent setter
+ Object.defineProperty(window.HTMLDivElement.prototype, 'innerText', {
+ set(value) { this.textContent = value; }
+ });
+
+ // Placeholder for feature detection
+ document.caretRangeFromPoint = () => null;
+
+ return dom;
+}
+
+/**
+ * @param {Element} element
+ * @param {string|undefined} selector
+ * @returns {?Element}
+ */
+function querySelectorChildOrSelf(element, selector) {
+ return selector ? element.querySelector(selector) : element;
+}
+
+/**
+ * @param {JSDOM} dom
+ * @param {?Node} node
+ * @returns {?Text|Node}
+ */
+function getChildTextNodeOrSelf(dom, node) {
+ if (node === null) { return null; }
+ const Node = dom.window.Node;
+ const childNode = node.firstChild;
+ return (childNode !== null && childNode.nodeType === Node.TEXT_NODE ? childNode : node);
+}
+
+/**
+ * @param {unknown} value
+ * @returns {unknown}
+ */
+function getPrototypeOfOrNull(value) {
+ try {
+ return Object.getPrototypeOf(value);
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * @param {Document} document
+ * @returns {?Element}
+ */
+function findImposterElement(document) {
+ // Finds the imposter element based on it's z-index style
+ return document.querySelector('div[style*="2147483646"]>*');
+}
+
+
+/** */
+async function testDocument1() {
+ const dom = createJSDOM(path.join(__dirname, 'data', 'html', 'test-document1.html'));
+ const window = dom.window;
+ const document = window.document;
+ const Node = window.Node;
+ const Range = window.Range;
+
+ const vm = new VM({document, window, Range, Node});
+ vm.execute([
+ 'js/data/sandbox/string-util.js',
+ 'js/dom/dom-text-scanner.js',
+ 'js/dom/text-source-range.js',
+ 'js/dom/text-source-element.js',
+ 'js/dom/document-util.js'
+ ]);
+ /** @type {[DOMTextScanner: typeof DOMTextScanner, TextSourceRange: typeof TextSourceRange, TextSourceElement: typeof TextSourceElement, DocumentUtil: typeof DocumentUtil]} */
+ const [DOMTextScanner2, TextSourceRange2, TextSourceElement2, DocumentUtil2] = vm.get([
+ 'DOMTextScanner',
+ 'TextSourceRange',
+ 'TextSourceElement',
+ 'DocumentUtil'
+ ]);
+
+ try {
+ await testDocumentTextScanningFunctions(dom, {DocumentUtil: DocumentUtil2, TextSourceRange: TextSourceRange2, TextSourceElement: TextSourceElement2});
+ await testTextSourceRangeSeekFunctions(dom, {DOMTextScanner: DOMTextScanner2});
+ } finally {
+ window.close();
+ }
+}
+
+/**
+ * @param {JSDOM} dom
+ * @param {{DocumentUtil: typeof DocumentUtil, TextSourceRange: typeof TextSourceRange, TextSourceElement: typeof TextSourceElement}} details
+ */
+async function testDocumentTextScanningFunctions(dom, {DocumentUtil, TextSourceRange, TextSourceElement}) {
+ const document = dom.window.document;
+
+ for (const testElement of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.test[data-test-type=scan]'))) {
+ // Get test parameters
+ const {
+ elementFromPointSelector,
+ caretRangeFromPointSelector,
+ startNodeSelector,
+ startOffset,
+ endNodeSelector,
+ endOffset,
+ resultType,
+ sentenceScanExtent,
+ sentence,
+ hasImposter,
+ terminateAtNewlines
+ } = testElement.dataset;
+
+ const elementFromPointValue = querySelectorChildOrSelf(testElement, elementFromPointSelector);
+ const caretRangeFromPointValue = querySelectorChildOrSelf(testElement, caretRangeFromPointSelector);
+ const startNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, startNodeSelector));
+ const endNode = getChildTextNodeOrSelf(dom, querySelectorChildOrSelf(testElement, endNodeSelector));
+
+ const startOffset2 = parseInt(/** @type {string} */ (startOffset), 10);
+ const endOffset2 = parseInt(/** @type {string} */ (endOffset), 10);
+ const sentenceScanExtent2 = parseInt(/** @type {string} */ (sentenceScanExtent), 10);
+ const terminateAtNewlines2 = (terminateAtNewlines !== 'false');
+
+ assert.notStrictEqual(elementFromPointValue, null);
+ assert.notStrictEqual(caretRangeFromPointValue, null);
+ assert.notStrictEqual(startNode, null);
+ assert.notStrictEqual(endNode, null);
+
+ // Setup functions
+ document.elementFromPoint = () => elementFromPointValue;
+
+ document.caretRangeFromPoint = (x, y) => {
+ const imposter = getChildTextNodeOrSelf(dom, findImposterElement(document));
+ assert.strictEqual(!!imposter, hasImposter === 'true');
+
+ const range = document.createRange();
+ range.setStart(/** @type {Node} */ (imposter ? imposter : startNode), startOffset2);
+ range.setEnd(/** @type {Node} */ (imposter ? imposter : startNode), endOffset2);
+
+ // Override getClientRects to return a rect guaranteed to contain (x, y)
+ range.getClientRects = () => {
+ /** @type {import('test/document-types').PseudoDOMRectList} */
+ const domRectList = Object.assign(
+ [new DOMRect(x - 1, y - 1, 2, 2)],
+ {
+ /**
+ * @this {DOMRect[]}
+ * @param {number} index
+ * @returns {DOMRect}
+ */
+ item: function item(index) { return this[index]; }
+ }
+ );
+ return domRectList;
+ };
+ return range;
+ };
+
+ // Test docRangeFromPoint
+ const source = DocumentUtil.getRangeFromPoint(0, 0, {
+ deepContentScan: false,
+ normalizeCssZoom: true
+ });
+ switch (resultType) {
+ case 'TextSourceRange':
+ assert.strictEqual(getPrototypeOfOrNull(source), TextSourceRange.prototype);
+ break;
+ case 'TextSourceElement':
+ assert.strictEqual(getPrototypeOfOrNull(source), TextSourceElement.prototype);
+ break;
+ case 'null':
+ assert.strictEqual(source, null);
+ break;
+ default:
+ assert.ok(false);
+ break;
+ }
+ if (source === null) { continue; }
+
+ // Sentence info
+ const terminatorString = '…。..??!!';
+ const terminatorMap = new Map();
+ for (const char of terminatorString) {
+ terminatorMap.set(char, [false, true]);
+ }
+ const quoteArray = [['「', '」'], ['『', '』'], ['\'', '\''], ['"', '"']];
+ const forwardQuoteMap = new Map();
+ const backwardQuoteMap = new Map();
+ for (const [char1, char2] of quoteArray) {
+ forwardQuoteMap.set(char1, [char2, false]);
+ backwardQuoteMap.set(char2, [char1, false]);
+ }
+
+ // Test docSentenceExtract
+ const sentenceActual = DocumentUtil.extractSentence(
+ source,
+ false,
+ sentenceScanExtent2,
+ terminateAtNewlines2,
+ terminatorMap,
+ forwardQuoteMap,
+ backwardQuoteMap
+ ).text;
+ assert.strictEqual(sentenceActual, sentence);
+
+ // Clean
+ source.cleanup();
+ }
+}
+
+/**
+ * @param {JSDOM} dom
+ * @param {{DOMTextScanner: typeof DOMTextScanner}} details
+ */
+async function testTextSourceRangeSeekFunctions(dom, {DOMTextScanner}) {
+ const document = dom.window.document;
+
+ for (const testElement of /** @type {NodeListOf<HTMLElement>} */ (document.querySelectorAll('.test[data-test-type=text-source-range-seek]'))) {
+ // Get test parameters
+ const {
+ seekNodeSelector,
+ seekNodeIsText,
+ seekOffset,
+ seekLength,
+ seekDirection,
+ expectedResultNodeSelector,
+ expectedResultNodeIsText,
+ expectedResultOffset,
+ expectedResultContent
+ } = testElement.dataset;
+
+ const seekOffset2 = parseInt(/** @type {string} */ (seekOffset), 10);
+ const seekLength2 = parseInt(/** @type {string} */ (seekLength), 10);
+ const expectedResultOffset2 = parseInt(/** @type {string} */ (expectedResultOffset), 10);
+
+ /** @type {?Node} */
+ let seekNode = testElement.querySelector(/** @type {string} */ (seekNodeSelector));
+ if (seekNodeIsText === 'true' && seekNode !== null) {
+ seekNode = seekNode.firstChild;
+ }
+
+ /** @type {?Node} */
+ let expectedResultNode = testElement.querySelector(/** @type {string} */ (expectedResultNodeSelector));
+ if (expectedResultNodeIsText === 'true' && expectedResultNode !== null) {
+ expectedResultNode = expectedResultNode.firstChild;
+ }
+
+ const {node, offset, content} = (
+ seekDirection === 'forward' ?
+ new DOMTextScanner(/** @type {Node} */ (seekNode), seekOffset2, true, false).seek(seekLength2) :
+ new DOMTextScanner(/** @type {Node} */ (seekNode), seekOffset2, true, false).seek(-seekLength2)
+ );
+
+ assert.strictEqual(node, expectedResultNode);
+ assert.strictEqual(offset, expectedResultOffset2);
+ assert.strictEqual(content, expectedResultContent);
+ }
+}
+
+
+/** */
+async function main() {
+ await testDocument1();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-hotkey-util.js b/test/test-hotkey-util.js
new file mode 100644
index 00000000..88f5a8d2
--- /dev/null
+++ b/test/test-hotkey-util.js
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+
+/**
+ * @template T
+ * @param {T} value
+ * @returns {T}
+ */
+function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+/**
+ * @returns {HotkeyUtil}
+ */
+function createHotkeyUtil() {
+ const vm = new VM();
+ vm.execute(['js/input/hotkey-util.js']);
+ /** @type {typeof HotkeyUtil} */
+ const HotkeyUtil2 = vm.getSingle('HotkeyUtil');
+ return new HotkeyUtil2();
+}
+
+
+/** */
+function testCommandConversions() {
+ /** @type {{os: import('environment').OperatingSystem, command: string, expectedCommand: string, expectedInput: {key: string, modifiers: import('input').Modifier[]}}[]} */
+ const data = [
+ {os: 'win', command: 'Alt+F', expectedCommand: 'Alt+F', expectedInput: {key: 'KeyF', modifiers: ['alt']}},
+ {os: 'win', command: 'F1', expectedCommand: 'F1', expectedInput: {key: 'F1', modifiers: []}},
+
+ {os: 'win', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'win', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'win', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
+
+ {os: 'mac', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
+ {os: 'mac', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'MacCtrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'mac', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}},
+
+ {os: 'linux', command: 'Ctrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'linux', command: 'MacCtrl+Alt+Shift+F1', expectedCommand: 'Ctrl+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['ctrl', 'alt', 'shift']}},
+ {os: 'linux', command: 'Command+Alt+Shift+F1', expectedCommand: 'Command+Alt+Shift+F1', expectedInput: {key: 'F1', modifiers: ['meta', 'alt', 'shift']}}
+ ];
+
+ const hotkeyUtil = createHotkeyUtil();
+ for (const {command, os, expectedInput, expectedCommand} of data) {
+ hotkeyUtil.os = os;
+ const input = clone(hotkeyUtil.convertCommandToInput(command));
+ assert.deepStrictEqual(input, expectedInput);
+ const command2 = hotkeyUtil.convertInputToCommand(input.key, input.modifiers);
+ assert.deepStrictEqual(command2, expectedCommand);
+ }
+}
+
+/** */
+function testDisplayNames() {
+ /** @type {{os: import('environment').OperatingSystem, key: ?string, modifiers: import('input').Modifier[], expected: string}[]} */
+ const data = [
+ {os: 'win', key: null, modifiers: [], expected: ''},
+ {os: 'win', key: 'KeyF', modifiers: [], expected: 'F'},
+ {os: 'win', key: 'F1', modifiers: [], expected: 'F1'},
+ {os: 'win', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
+ {os: 'win', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
+ {os: 'win', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
+ {os: 'win', key: null, modifiers: ['alt'], expected: 'Alt'},
+ {os: 'win', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
+ {os: 'win', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
+ {os: 'win', key: null, modifiers: ['shift'], expected: 'Shift'},
+ {os: 'win', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
+ {os: 'win', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
+ {os: 'win', key: null, modifiers: ['meta'], expected: 'Windows'},
+ {os: 'win', key: 'KeyF', modifiers: ['meta'], expected: 'Windows + F'},
+ {os: 'win', key: 'F1', modifiers: ['meta'], expected: 'Windows + F1'},
+ {os: 'win', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
+ {os: 'win', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
+ {os: 'win', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
+
+ {os: 'mac', key: null, modifiers: [], expected: ''},
+ {os: 'mac', key: 'KeyF', modifiers: [], expected: 'F'},
+ {os: 'mac', key: 'F1', modifiers: [], expected: 'F1'},
+ {os: 'mac', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
+ {os: 'mac', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
+ {os: 'mac', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
+ {os: 'mac', key: null, modifiers: ['alt'], expected: 'Opt'},
+ {os: 'mac', key: 'KeyF', modifiers: ['alt'], expected: 'Opt + F'},
+ {os: 'mac', key: 'F1', modifiers: ['alt'], expected: 'Opt + F1'},
+ {os: 'mac', key: null, modifiers: ['shift'], expected: 'Shift'},
+ {os: 'mac', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
+ {os: 'mac', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
+ {os: 'mac', key: null, modifiers: ['meta'], expected: 'Cmd'},
+ {os: 'mac', key: 'KeyF', modifiers: ['meta'], expected: 'Cmd + F'},
+ {os: 'mac', key: 'F1', modifiers: ['meta'], expected: 'Cmd + F1'},
+ {os: 'mac', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
+ {os: 'mac', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
+ {os: 'mac', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
+
+ {os: 'linux', key: null, modifiers: [], expected: ''},
+ {os: 'linux', key: 'KeyF', modifiers: [], expected: 'F'},
+ {os: 'linux', key: 'F1', modifiers: [], expected: 'F1'},
+ {os: 'linux', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
+ {os: 'linux', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
+ {os: 'linux', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
+ {os: 'linux', key: null, modifiers: ['alt'], expected: 'Alt'},
+ {os: 'linux', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
+ {os: 'linux', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
+ {os: 'linux', key: null, modifiers: ['shift'], expected: 'Shift'},
+ {os: 'linux', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
+ {os: 'linux', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
+ {os: 'linux', key: null, modifiers: ['meta'], expected: 'Super'},
+ {os: 'linux', key: 'KeyF', modifiers: ['meta'], expected: 'Super + F'},
+ {os: 'linux', key: 'F1', modifiers: ['meta'], expected: 'Super + F1'},
+ {os: 'linux', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
+ {os: 'linux', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
+ {os: 'linux', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'},
+
+ {os: 'unknown', key: null, modifiers: [], expected: ''},
+ {os: 'unknown', key: 'KeyF', modifiers: [], expected: 'F'},
+ {os: 'unknown', key: 'F1', modifiers: [], expected: 'F1'},
+ {os: 'unknown', key: null, modifiers: ['ctrl'], expected: 'Ctrl'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['ctrl'], expected: 'Ctrl + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['ctrl'], expected: 'Ctrl + F1'},
+ {os: 'unknown', key: null, modifiers: ['alt'], expected: 'Alt'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['alt'], expected: 'Alt + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['alt'], expected: 'Alt + F1'},
+ {os: 'unknown', key: null, modifiers: ['shift'], expected: 'Shift'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['shift'], expected: 'Shift + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['shift'], expected: 'Shift + F1'},
+ {os: 'unknown', key: null, modifiers: ['meta'], expected: 'Meta'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['meta'], expected: 'Meta + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['meta'], expected: 'Meta + F1'},
+ {os: 'unknown', key: null, modifiers: ['mouse1'], expected: 'Mouse 1'},
+ {os: 'unknown', key: 'KeyF', modifiers: ['mouse1'], expected: 'Mouse 1 + F'},
+ {os: 'unknown', key: 'F1', modifiers: ['mouse1'], expected: 'Mouse 1 + F1'}
+ ];
+
+ const hotkeyUtil = createHotkeyUtil();
+ for (const {os, key, modifiers, expected} of data) {
+ hotkeyUtil.os = os;
+ const displayName = hotkeyUtil.getInputDisplayValue(key, modifiers);
+ assert.deepStrictEqual(displayName, expected);
+ }
+}
+
+/** */
+function testSortModifiers() {
+ /** @type {{modifiers: import('input').Modifier[], expected: import('input').Modifier[]}[]} */
+ const data = [
+ {modifiers: [], expected: []},
+ {modifiers: ['shift', 'alt', 'ctrl', 'mouse4', 'meta', 'mouse1', 'mouse0'], expected: ['meta', 'ctrl', 'alt', 'shift', 'mouse0', 'mouse1', 'mouse4']}
+ ];
+
+ const hotkeyUtil = createHotkeyUtil();
+ for (const {modifiers, expected} of data) {
+ const modifiers2 = hotkeyUtil.sortModifiers(modifiers);
+ assert.strictEqual(modifiers2, modifiers);
+ assert.deepStrictEqual(modifiers2, expected);
+ }
+}
+
+
+/** */
+function main() {
+ testCommandConversions();
+ testDisplayNames();
+ testSortModifiers();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-japanese-util.js b/test/test-japanese-util.js
new file mode 100644
index 00000000..0a95b858
--- /dev/null
+++ b/test/test-japanese-util.js
@@ -0,0 +1,915 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+const vm = new VM();
+vm.execute([
+ 'lib/wanakana.min.js',
+ 'js/language/sandbox/japanese-util.js',
+ 'js/general/text-source-map.js'
+]);
+/** @type {[JapaneseUtil: typeof JapaneseUtil, TextSourceMap: typeof TextSourceMap, wanakana: import('wanakana')]} */
+const [JapaneseUtil2, TextSourceMap2, wanakana] = vm.get(['JapaneseUtil', 'TextSourceMap', 'wanakana']);
+const jp = new JapaneseUtil2(wanakana);
+
+
+/** */
+function testIsCodePointKanji() {
+ /** @type {[characters: string, expected: boolean][]} */
+ const data = [
+ ['力方', true],
+ ['\u53f1\u{20b9f}', true],
+ ['かたカタ々kata、。?,.?', false],
+ ['逸逸', true]
+ ];
+
+ for (const [characters, expected] of data) {
+ for (const character of characters) {
+ const codePoint = /** @type {number} */ (character.codePointAt(0));
+ const actual = jp.isCodePointKanji(codePoint);
+ assert.strictEqual(actual, expected, `isCodePointKanji failed for ${character} (\\u{${codePoint.toString(16)}})`);
+ }
+ }
+}
+
+/** */
+function testIsCodePointKana() {
+ /** @type {[characters: string, expected: boolean][]} */
+ const data = [
+ ['かたカタ', true],
+ ['力方々kata、。?,.?', false],
+ ['\u53f1\u{20b9f}', false]
+ ];
+
+ for (const [characters, expected] of data) {
+ for (const character of characters) {
+ const codePoint = /** @type {number} */ (character.codePointAt(0));
+ const actual = jp.isCodePointKana(codePoint);
+ assert.strictEqual(actual, expected, `isCodePointKana failed for ${character} (\\u{${codePoint.toString(16)}})`);
+ }
+ }
+}
+
+/** */
+function testIsCodePointJapanese() {
+ /** @type {[characters: string, expected: boolean][]} */
+ const data = [
+ ['かたカタ力方々、。?', true],
+ ['\u53f1\u{20b9f}', true],
+ ['kata,.?', false],
+ ['逸逸', true]
+ ];
+
+ for (const [characters, expected] of data) {
+ for (const character of characters) {
+ const codePoint = /** @type {number} */ (character.codePointAt(0));
+ const actual = jp.isCodePointJapanese(codePoint);
+ assert.strictEqual(actual, expected, `isCodePointJapanese failed for ${character} (\\u{${codePoint.toString(16)}})`);
+ }
+ }
+}
+
+/** */
+function testIsStringEntirelyKana() {
+ /** @type {[string: string, expected: boolean][]} */
+ const data = [
+ ['かたかな', true],
+ ['カタカナ', true],
+ ['ひらがな', true],
+ ['ヒラガナ', true],
+ ['カタカナひらがな', true],
+ ['かたカタ力方々、。?', false],
+ ['\u53f1\u{20b9f}', false],
+ ['kata,.?', false],
+ ['かたカタ力方々、。?invalid', false],
+ ['\u53f1\u{20b9f}invalid', false],
+ ['kata,.?かた', false]
+ ];
+
+ for (const [string, expected] of data) {
+ assert.strictEqual(jp.isStringEntirelyKana(string), expected);
+ }
+}
+
+/** */
+function testIsStringPartiallyJapanese() {
+ /** @type {[string: string, expected: boolean][]} */
+ const data = [
+ ['かたかな', true],
+ ['カタカナ', true],
+ ['ひらがな', true],
+ ['ヒラガナ', true],
+ ['カタカナひらがな', true],
+ ['かたカタ力方々、。?', true],
+ ['\u53f1\u{20b9f}', true],
+ ['kata,.?', false],
+ ['かたカタ力方々、。?invalid', true],
+ ['\u53f1\u{20b9f}invalid', true],
+ ['kata,.?かた', true],
+ ['逸逸', true]
+ ];
+
+ for (const [string, expected] of data) {
+ assert.strictEqual(jp.isStringPartiallyJapanese(string), expected);
+ }
+}
+
+/** */
+function testConvertKatakanaToHiragana() {
+ /** @type {[string: string, expected: string, keepProlongedSoundMarks?: boolean][]} */
+ const data = [
+ ['かたかな', 'かたかな'],
+ ['ひらがな', 'ひらがな'],
+ ['カタカナ', 'かたかな'],
+ ['ヒラガナ', 'ひらがな'],
+ ['カタカナかたかな', 'かたかなかたかな'],
+ ['ヒラガナひらがな', 'ひらがなひらがな'],
+ ['chikaraちからチカラ力', 'chikaraちからちから力'],
+ ['katakana', 'katakana'],
+ ['hiragana', 'hiragana'],
+ ['カーナー', 'かあなあ'],
+ ['カーナー', 'かーなー', true]
+ ];
+
+ for (const [string, expected, keepProlongedSoundMarks=false] of data) {
+ assert.strictEqual(jp.convertKatakanaToHiragana(string, keepProlongedSoundMarks), expected);
+ }
+}
+
+/** */
+function testConvertHiraganaToKatakana() {
+ /** @type {[string: string, expected: string][]} */
+ const data = [
+ ['かたかな', 'カタカナ'],
+ ['ひらがな', 'ヒラガナ'],
+ ['カタカナ', 'カタカナ'],
+ ['ヒラガナ', 'ヒラガナ'],
+ ['カタカナかたかな', 'カタカナカタカナ'],
+ ['ヒラガナひらがな', 'ヒラガナヒラガナ'],
+ ['chikaraちからチカラ力', 'chikaraチカラチカラ力'],
+ ['katakana', 'katakana'],
+ ['hiragana', 'hiragana']
+ ];
+
+ for (const [string, expected] of data) {
+ assert.strictEqual(jp.convertHiraganaToKatakana(string), expected);
+ }
+}
+
+/** */
+function testConvertToRomaji() {
+ /** @type {[string: string, expected: string][]} */
+ const data = [
+ ['かたかな', 'katakana'],
+ ['ひらがな', 'hiragana'],
+ ['カタカナ', 'katakana'],
+ ['ヒラガナ', 'hiragana'],
+ ['カタカナかたかな', 'katakanakatakana'],
+ ['ヒラガナひらがな', 'hiraganahiragana'],
+ ['chikaraちからチカラ力', 'chikarachikarachikara力'],
+ ['katakana', 'katakana'],
+ ['hiragana', 'hiragana']
+ ];
+
+ for (const [string, expected] of data) {
+ assert.strictEqual(jp.convertToRomaji(string), expected);
+ }
+}
+
+/** */
+function testConvertNumericToFullWidth() {
+ /** @type {[string: string, expected: string][]} */
+ const data = [
+ ['0123456789', '0123456789'],
+ ['abcdefghij', 'abcdefghij'],
+ ['カタカナ', 'カタカナ'],
+ ['ひらがな', 'ひらがな']
+ ];
+
+ for (const [string, expected] of data) {
+ assert.strictEqual(jp.convertNumericToFullWidth(string), expected);
+ }
+}
+
+/** */
+function testConvertHalfWidthKanaToFullWidth() {
+ /** @type {[string: string, expected: string, expectedSourceMapping?: number[]][]} */
+ const data = [
+ ['0123456789', '0123456789'],
+ ['abcdefghij', 'abcdefghij'],
+ ['カタカナ', 'カタカナ'],
+ ['ひらがな', 'ひらがな'],
+ ['カキ', 'カキ', [1, 1]],
+ ['ガキ', 'ガキ', [2, 1]],
+ ['ニホン', 'ニホン', [1, 1, 1]],
+ ['ニッポン', 'ニッポン', [1, 1, 2, 1]]
+ ];
+
+ for (const [string, expected, expectedSourceMapping] of data) {
+ const sourceMap = new TextSourceMap2(string);
+ const actual1 = jp.convertHalfWidthKanaToFullWidth(string, null);
+ const actual2 = jp.convertHalfWidthKanaToFullWidth(string, sourceMap);
+ assert.strictEqual(actual1, expected);
+ assert.strictEqual(actual2, expected);
+ if (typeof expectedSourceMapping !== 'undefined') {
+ assert.ok(sourceMap.equals(new TextSourceMap2(string, expectedSourceMapping)));
+ }
+ }
+}
+
+/** */
+function testConvertAlphabeticToKana() {
+ /** @type {[string: string, expected: string, expectedSourceMapping?: number[]][]} */
+ const data = [
+ ['0123456789', '0123456789'],
+ ['abcdefghij', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]],
+ ['ABCDEFGHIJ', 'あbcでfgひj', [1, 1, 1, 2, 1, 1, 2, 1]], // wanakana.toHiragana converts text to lower case
+ ['カタカナ', 'カタカナ'],
+ ['ひらがな', 'ひらがな'],
+ ['chikara', 'ちから', [3, 2, 2]],
+ ['CHIKARA', 'ちから', [3, 2, 2]]
+ ];
+
+ for (const [string, expected, expectedSourceMapping] of data) {
+ const sourceMap = new TextSourceMap2(string);
+ const actual1 = jp.convertAlphabeticToKana(string, null);
+ const actual2 = jp.convertAlphabeticToKana(string, sourceMap);
+ assert.strictEqual(actual1, expected);
+ assert.strictEqual(actual2, expected);
+ if (typeof expectedSourceMapping !== 'undefined') {
+ assert.ok(sourceMap.equals(new TextSourceMap2(string, expectedSourceMapping)));
+ }
+ }
+}
+
+/** */
+function testDistributeFurigana() {
+ /** @type {[input: [term: string, reading: string], expected: {text: string, reading: string}[]][]} */
+ const data = [
+ ([
+ ['有り難う', 'ありがとう'],
+ [
+ {text: '有', reading: 'あ'},
+ {text: 'り', reading: ''},
+ {text: '難', reading: 'がと'},
+ {text: 'う', reading: ''}
+ ]
+ ]),
+ [
+ ['方々', 'かたがた'],
+ [
+ {text: '方々', reading: 'かたがた'}
+ ]
+ ],
+ [
+ ['お祝い', 'おいわい'],
+ [
+ {text: 'お', reading: ''},
+ {text: '祝', reading: 'いわ'},
+ {text: 'い', reading: ''}
+ ]
+ ],
+ [
+ ['美味しい', 'おいしい'],
+ [
+ {text: '美味', reading: 'おい'},
+ {text: 'しい', reading: ''}
+ ]
+ ],
+ [
+ ['食べ物', 'たべもの'],
+ [
+ {text: '食', reading: 'た'},
+ {text: 'べ', reading: ''},
+ {text: '物', reading: 'もの'}
+ ]
+ ],
+ [
+ ['試し切り', 'ためしぎり'],
+ [
+ {text: '試', reading: 'ため'},
+ {text: 'し', reading: ''},
+ {text: '切', reading: 'ぎ'},
+ {text: 'り', reading: ''}
+ ]
+ ],
+ // Ambiguous
+ [
+ ['飼い犬', 'かいいぬ'],
+ [
+ {text: '飼い犬', reading: 'かいいぬ'}
+ ]
+ ],
+ [
+ ['長い間', 'ながいあいだ'],
+ [
+ {text: '長い間', reading: 'ながいあいだ'}
+ ]
+ ],
+ // Same/empty reading
+ [
+ ['飼い犬', ''],
+ [
+ {text: '飼い犬', reading: ''}
+ ]
+ ],
+ [
+ ['かいいぬ', 'かいいぬ'],
+ [
+ {text: 'かいいぬ', reading: ''}
+ ]
+ ],
+ [
+ ['かいぬ', 'かいぬ'],
+ [
+ {text: 'かいぬ', reading: ''}
+ ]
+ ],
+ // Misc
+ [
+ ['月', 'か'],
+ [
+ {text: '月', reading: 'か'}
+ ]
+ ],
+ [
+ ['月', 'カ'],
+ [
+ {text: '月', reading: 'カ'}
+ ]
+ ],
+ // Mismatched kana readings
+ [
+ ['有り難う', 'アリガトウ'],
+ [
+ {text: '有', reading: 'ア'},
+ {text: 'り', reading: 'リ'},
+ {text: '難', reading: 'ガト'},
+ {text: 'う', reading: 'ウ'}
+ ]
+ ],
+ [
+ ['ありがとう', 'アリガトウ'],
+ [
+ {text: 'ありがとう', reading: 'アリガトウ'}
+ ]
+ ],
+ // Mismatched kana readings (real examples)
+ [
+ ['カ月', 'かげつ'],
+ [
+ {text: 'カ', reading: 'か'},
+ {text: '月', reading: 'げつ'}
+ ]
+ ],
+ [
+ ['序ノ口', 'じょのくち'],
+ [
+ {text: '序', reading: 'じょ'},
+ {text: 'ノ', reading: 'の'},
+ {text: '口', reading: 'くち'}
+ ]
+ ],
+ [
+ ['スズメの涙', 'すずめのなみだ'],
+ [
+ {text: 'スズメ', reading: 'すずめ'},
+ {text: 'の', reading: ''},
+ {text: '涙', reading: 'なみだ'}
+ ]
+ ],
+ [
+ ['二カ所', 'にかしょ'],
+ [
+ {text: '二', reading: 'に'},
+ {text: 'カ', reading: 'か'},
+ {text: '所', reading: 'しょ'}
+ ]
+ ],
+ [
+ ['八ツ橋', 'やつはし'],
+ [
+ {text: '八', reading: 'や'},
+ {text: 'ツ', reading: 'つ'},
+ {text: '橋', reading: 'はし'}
+ ]
+ ],
+ [
+ ['八ツ橋', 'やつはし'],
+ [
+ {text: '八', reading: 'や'},
+ {text: 'ツ', reading: 'つ'},
+ {text: '橋', reading: 'はし'}
+ ]
+ ],
+ [
+ ['一カ月', 'いっかげつ'],
+ [
+ {text: '一', reading: 'いっ'},
+ {text: 'カ', reading: 'か'},
+ {text: '月', reading: 'げつ'}
+ ]
+ ],
+ [
+ ['一カ所', 'いっかしょ'],
+ [
+ {text: '一', reading: 'いっ'},
+ {text: 'カ', reading: 'か'},
+ {text: '所', reading: 'しょ'}
+ ]
+ ],
+ [
+ ['カ所', 'かしょ'],
+ [
+ {text: 'カ', reading: 'か'},
+ {text: '所', reading: 'しょ'}
+ ]
+ ],
+ [
+ ['数カ月', 'すうかげつ'],
+ [
+ {text: '数', reading: 'すう'},
+ {text: 'カ', reading: 'か'},
+ {text: '月', reading: 'げつ'}
+ ]
+ ],
+ [
+ ['くノ一', 'くのいち'],
+ [
+ {text: 'く', reading: ''},
+ {text: 'ノ', reading: 'の'},
+ {text: '一', reading: 'いち'}
+ ]
+ ],
+ [
+ ['くノ一', 'くのいち'],
+ [
+ {text: 'く', reading: ''},
+ {text: 'ノ', reading: 'の'},
+ {text: '一', reading: 'いち'}
+ ]
+ ],
+ [
+ ['数カ国', 'すうかこく'],
+ [
+ {text: '数', reading: 'すう'},
+ {text: 'カ', reading: 'か'},
+ {text: '国', reading: 'こく'}
+ ]
+ ],
+ [
+ ['数カ所', 'すうかしょ'],
+ [
+ {text: '数', reading: 'すう'},
+ {text: 'カ', reading: 'か'},
+ {text: '所', reading: 'しょ'}
+ ]
+ ],
+ [
+ ['壇ノ浦の戦い', 'だんのうらのたたかい'],
+ [
+ {text: '壇', reading: 'だん'},
+ {text: 'ノ', reading: 'の'},
+ {text: '浦', reading: 'うら'},
+ {text: 'の', reading: ''},
+ {text: '戦', reading: 'たたか'},
+ {text: 'い', reading: ''}
+ ]
+ ],
+ [
+ ['壇ノ浦の戦', 'だんのうらのたたかい'],
+ [
+ {text: '壇', reading: 'だん'},
+ {text: 'ノ', reading: 'の'},
+ {text: '浦', reading: 'うら'},
+ {text: 'の', reading: ''},
+ {text: '戦', reading: 'たたかい'}
+ ]
+ ],
+ [
+ ['序ノ口格', 'じょのくちかく'],
+ [
+ {text: '序', reading: 'じょ'},
+ {text: 'ノ', reading: 'の'},
+ {text: '口格', reading: 'くちかく'}
+ ]
+ ],
+ [
+ ['二カ国語', 'にかこくご'],
+ [
+ {text: '二', reading: 'に'},
+ {text: 'カ', reading: 'か'},
+ {text: '国語', reading: 'こくご'}
+ ]
+ ],
+ [
+ ['カ国', 'かこく'],
+ [
+ {text: 'カ', reading: 'か'},
+ {text: '国', reading: 'こく'}
+ ]
+ ],
+ [
+ ['カ国語', 'かこくご'],
+ [
+ {text: 'カ', reading: 'か'},
+ {text: '国語', reading: 'こくご'}
+ ]
+ ],
+ [
+ ['壇ノ浦の合戦', 'だんのうらのかっせん'],
+ [
+ {text: '壇', reading: 'だん'},
+ {text: 'ノ', reading: 'の'},
+ {text: '浦', reading: 'うら'},
+ {text: 'の', reading: ''},
+ {text: '合戦', reading: 'かっせん'}
+ ]
+ ],
+ [
+ ['一タ偏', 'いちたへん'],
+ [
+ {text: '一', reading: 'いち'},
+ {text: 'タ', reading: 'た'},
+ {text: '偏', reading: 'へん'}
+ ]
+ ],
+ [
+ ['ル又', 'るまた'],
+ [
+ {text: 'ル', reading: 'る'},
+ {text: '又', reading: 'また'}
+ ]
+ ],
+ [
+ ['ノ木偏', 'のぎへん'],
+ [
+ {text: 'ノ', reading: 'の'},
+ {text: '木偏', reading: 'ぎへん'}
+ ]
+ ],
+ [
+ ['一ノ貝', 'いちのかい'],
+ [
+ {text: '一', reading: 'いち'},
+ {text: 'ノ', reading: 'の'},
+ {text: '貝', reading: 'かい'}
+ ]
+ ],
+ [
+ ['虎ノ門事件', 'とらのもんじけん'],
+ [
+ {text: '虎', reading: 'とら'},
+ {text: 'ノ', reading: 'の'},
+ {text: '門事件', reading: 'もんじけん'}
+ ]
+ ],
+ [
+ ['教育ニ関スル勅語', 'きょういくにかんするちょくご'],
+ [
+ {text: '教育', reading: 'きょういく'},
+ {text: 'ニ', reading: 'に'},
+ {text: '関', reading: 'かん'},
+ {text: 'スル', reading: 'する'},
+ {text: '勅語', reading: 'ちょくご'}
+ ]
+ ],
+ [
+ ['二カ年', 'にかねん'],
+ [
+ {text: '二', reading: 'に'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['三カ年', 'さんかねん'],
+ [
+ {text: '三', reading: 'さん'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['四カ年', 'よんかねん'],
+ [
+ {text: '四', reading: 'よん'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['五カ年', 'ごかねん'],
+ [
+ {text: '五', reading: 'ご'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['六カ年', 'ろっかねん'],
+ [
+ {text: '六', reading: 'ろっ'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['七カ年', 'ななかねん'],
+ [
+ {text: '七', reading: 'なな'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['八カ年', 'はちかねん'],
+ [
+ {text: '八', reading: 'はち'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['九カ年', 'きゅうかねん'],
+ [
+ {text: '九', reading: 'きゅう'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['十カ年', 'じゅうかねん'],
+ [
+ {text: '十', reading: 'じゅう'},
+ {text: 'カ', reading: 'か'},
+ {text: '年', reading: 'ねん'}
+ ]
+ ],
+ [
+ ['鏡ノ間', 'かがみのま'],
+ [
+ {text: '鏡', reading: 'かがみ'},
+ {text: 'ノ', reading: 'の'},
+ {text: '間', reading: 'ま'}
+ ]
+ ],
+ [
+ ['鏡ノ間', 'かがみのま'],
+ [
+ {text: '鏡', reading: 'かがみ'},
+ {text: 'ノ', reading: 'の'},
+ {text: '間', reading: 'ま'}
+ ]
+ ],
+ [
+ ['ページ違反', 'ぺーじいはん'],
+ [
+ {text: 'ペ', reading: 'ぺ'},
+ {text: 'ー', reading: ''},
+ {text: 'ジ', reading: 'じ'},
+ {text: '違反', reading: 'いはん'}
+ ]
+ ],
+ // Mismatched kana
+ [
+ ['サボる', 'サボル'],
+ [
+ {text: 'サボ', reading: ''},
+ {text: 'る', reading: 'ル'}
+ ]
+ ],
+ // Reading starts with term, but has remainder characters
+ [
+ ['シック', 'シック・ビルしょうこうぐん'],
+ [
+ {text: 'シック', reading: 'シック・ビルしょうこうぐん'}
+ ]
+ ],
+ // Kanji distribution tests
+ [
+ ['逸らす', 'そらす'],
+ [
+ {text: '逸', reading: 'そ'},
+ {text: 'らす', reading: ''}
+ ]
+ ],
+ [
+ ['逸らす', 'そらす'],
+ [
+ {text: '逸', reading: 'そ'},
+ {text: 'らす', reading: ''}
+ ]
+ ]
+ ];
+
+ for (const [[term, reading], expected] of data) {
+ const actual = jp.distributeFurigana(term, reading);
+ vm.assert.deepStrictEqual(actual, expected);
+ }
+}
+
+/** */
+function testDistributeFuriganaInflected() {
+ /** @type {[input: [term: string, reading: string, source: string], expected: {text: string, reading: string}[]][]} */
+ const data = [
+ [
+ ['美味しい', 'おいしい', '美味しかた'],
+ [
+ {text: '美味', reading: 'おい'},
+ {text: 'しかた', reading: ''}
+ ]
+ ],
+ [
+ ['食べる', 'たべる', '食べた'],
+ [
+ {text: '食', reading: 'た'},
+ {text: 'べた', reading: ''}
+ ]
+ ],
+ [
+ ['迄に', 'までに', 'までに'],
+ [
+ {text: 'までに', reading: ''}
+ ]
+ ],
+ [
+ ['行う', 'おこなう', 'おこなわなかった'],
+ [
+ {text: 'おこなわなかった', reading: ''}
+ ]
+ ],
+ [
+ ['いい', 'いい', 'イイ'],
+ [
+ {text: 'イイ', reading: ''}
+ ]
+ ],
+ [
+ ['否か', 'いなか', '否カ'],
+ [
+ {text: '否', reading: 'いな'},
+ {text: 'カ', reading: 'か'}
+ ]
+ ]
+ ];
+
+ for (const [[term, reading, source], expected] of data) {
+ const actual = jp.distributeFuriganaInflected(term, reading, source);
+ vm.assert.deepStrictEqual(actual, expected);
+ }
+}
+
+/** */
+function testCollapseEmphaticSequences() {
+ /** @type {[input: [text: string, fullCollapse: boolean], output: [expected: string, expectedSourceMapping: number[]]][]} */
+ const data = [
+ [['かこい', false], ['かこい', [1, 1, 1]]],
+ [['かこい', true], ['かこい', [1, 1, 1]]],
+ [['かっこい', false], ['かっこい', [1, 1, 1, 1]]],
+ [['かっこい', true], ['かこい', [2, 1, 1]]],
+ [['かっっこい', false], ['かっこい', [1, 2, 1, 1]]],
+ [['かっっこい', true], ['かこい', [3, 1, 1]]],
+ [['かっっっこい', false], ['かっこい', [1, 3, 1, 1]]],
+ [['かっっっこい', true], ['かこい', [4, 1, 1]]],
+
+ [['こい', false], ['こい', [1, 1]]],
+ [['こい', true], ['こい', [1, 1]]],
+ [['っこい', false], ['っこい', [1, 1, 1]]],
+ [['っこい', true], ['こい', [2, 1]]],
+ [['っっこい', false], ['っこい', [2, 1, 1]]],
+ [['っっこい', true], ['こい', [3, 1]]],
+ [['っっっこい', false], ['っこい', [3, 1, 1]]],
+ [['っっっこい', true], ['こい', [4, 1]]],
+
+ [['すごい', false], ['すごい', [1, 1, 1]]],
+ [['すごい', true], ['すごい', [1, 1, 1]]],
+ [['すごーい', false], ['すごーい', [1, 1, 1, 1]]],
+ [['すごーい', true], ['すごい', [1, 2, 1]]],
+ [['すごーーい', false], ['すごーい', [1, 1, 2, 1]]],
+ [['すごーーい', true], ['すごい', [1, 3, 1]]],
+ [['すっごーい', false], ['すっごーい', [1, 1, 1, 1, 1]]],
+ [['すっごーい', true], ['すごい', [2, 2, 1]]],
+ [['すっっごーーい', false], ['すっごーい', [1, 2, 1, 2, 1]]],
+ [['すっっごーーい', true], ['すごい', [3, 3, 1]]],
+
+ [['', false], ['', []]],
+ [['', true], ['', []]],
+ [['っ', false], ['っ', [1]]],
+ [['っ', true], ['', [1]]],
+ [['っっ', false], ['っ', [2]]],
+ [['っっ', true], ['', [2]]],
+ [['っっっ', false], ['っ', [3]]],
+ [['っっっ', true], ['', [3]]]
+ ];
+
+ for (const [[text, fullCollapse], [expected, expectedSourceMapping]] of data) {
+ const sourceMap = new TextSourceMap2(text);
+ const actual1 = jp.collapseEmphaticSequences(text, fullCollapse, null);
+ const actual2 = jp.collapseEmphaticSequences(text, fullCollapse, sourceMap);
+ assert.strictEqual(actual1, expected);
+ assert.strictEqual(actual2, expected);
+ if (typeof expectedSourceMapping !== 'undefined') {
+ assert.ok(sourceMap.equals(new TextSourceMap2(text, expectedSourceMapping)));
+ }
+ }
+}
+
+/** */
+function testIsMoraPitchHigh() {
+ /** @type {[input: [moraIndex: number, pitchAccentDownstepPosition: number], expected: boolean][]} */
+ 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], false],
+ [[1, 2], true],
+ [[2, 2], false],
+ [[3, 2], false],
+
+ [[0, 3], false],
+ [[1, 3], true],
+ [[2, 3], true],
+ [[3, 3], false],
+
+ [[0, 4], false],
+ [[1, 4], true],
+ [[2, 4], true],
+ [[3, 4], true]
+ ];
+
+ for (const [[moraIndex, pitchAccentDownstepPosition], expected] of data) {
+ const actual = jp.isMoraPitchHigh(moraIndex, pitchAccentDownstepPosition);
+ assert.strictEqual(actual, expected);
+ }
+}
+
+/** */
+function testGetKanaMorae() {
+ /** @type {[text: string, expected: string[]][]} */
+ const data = [
+ ['かこ', ['か', 'こ']],
+ ['かっこ', ['か', 'っ', 'こ']],
+ ['カコ', ['カ', 'コ']],
+ ['カッコ', ['カ', 'ッ', 'コ']],
+ ['コート', ['コ', 'ー', 'ト']],
+ ['ちゃんと', ['ちゃ', 'ん', 'と']],
+ ['とうきょう', ['と', 'う', 'きょ', 'う']],
+ ['ぎゅう', ['ぎゅ', 'う']],
+ ['ディスコ', ['ディ', 'ス', 'コ']]
+ ];
+
+ for (const [text, expected] of data) {
+ const actual = jp.getKanaMorae(text);
+ vm.assert.deepStrictEqual(actual, expected);
+ }
+}
+
+
+/** */
+function main() {
+ testIsCodePointKanji();
+ testIsCodePointKana();
+ testIsCodePointJapanese();
+ testIsStringEntirelyKana();
+ testIsStringPartiallyJapanese();
+ testConvertKatakanaToHiragana();
+ testConvertHiraganaToKatakana();
+ testConvertToRomaji();
+ testConvertNumericToFullWidth();
+ testConvertHalfWidthKanaToFullWidth();
+ testConvertAlphabeticToKana();
+ testDistributeFurigana();
+ testDistributeFuriganaInflected();
+ testCollapseEmphaticSequences();
+ testIsMoraPitchHigh();
+ testGetKanaMorae();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-json-schema.js b/test/test-json-schema.js
new file mode 100644
index 00000000..f9cb023c
--- /dev/null
+++ b/test/test-json-schema.js
@@ -0,0 +1,1048 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+const vm = new VM();
+vm.execute([
+ 'js/core.js',
+ 'js/core/extension-error.js',
+ 'js/general/cache-map.js',
+ 'js/data/json-schema.js'
+]);
+/** @type {typeof JsonSchema} */
+const JsonSchema2 = vm.getSingle('JsonSchema');
+
+
+/**
+ * @param {import('json-schema').Schema} schema
+ * @param {unknown} value
+ * @returns {boolean}
+ */
+function schemaValidate(schema, value) {
+ return new JsonSchema2(schema).isValid(value);
+}
+
+/**
+ * @param {import('json-schema').Schema} schema
+ * @param {unknown} value
+ * @returns {import('json-schema').Value}
+ */
+function getValidValueOrDefault(schema, value) {
+ return new JsonSchema2(schema).getValidValueOrDefault(value);
+}
+
+/**
+ * @param {import('json-schema').Schema} schema
+ * @param {import('json-schema').Value} value
+ * @returns {import('json-schema').Value}
+ */
+function createProxy(schema, value) {
+ return new JsonSchema2(schema).createProxy(value);
+}
+
+/**
+ * @template T
+ * @param {T} value
+ * @returns {T}
+ */
+function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+
+/** */
+function testValidate1() {
+ /** @type {import('json-schema').Schema} */
+ const schema = {
+ allOf: [
+ {
+ type: 'number'
+ },
+ {
+ anyOf: [
+ {minimum: 10, maximum: 100},
+ {minimum: -100, maximum: -10}
+ ]
+ },
+ {
+ oneOf: [
+ {multipleOf: 3},
+ {multipleOf: 5}
+ ]
+ },
+ {
+ not: {
+ anyOf: [
+ {multipleOf: 20}
+ ]
+ }
+ }
+ ]
+ };
+
+ /**
+ * @param {number} value
+ * @returns {boolean}
+ */
+ const jsValidate = (value) => {
+ return (
+ typeof value === 'number' &&
+ (
+ (value >= 10 && value <= 100) ||
+ (value >= -100 && value <= -10)
+ ) &&
+ (
+ (
+ (value % 3) === 0 ||
+ (value % 5) === 0
+ ) &&
+ (value % 15) !== 0
+ ) &&
+ (value % 20) !== 0
+ );
+ };
+
+ for (let i = -111; i <= 111; i++) {
+ const actual = schemaValidate(schema, i);
+ const expected = jsValidate(i);
+ assert.strictEqual(actual, expected, `i=${i}; expected=${expected}; actual=${actual}`);
+ }
+}
+
+/** */
+function testValidate2() {
+ /** @type {{schema: import('json-schema').Schema, inputs: {expected: boolean, value: unknown}[]}[]} */
+ const data = [
+ // String tests
+ {
+ schema: {
+ type: 'string'
+ },
+ inputs: [
+ {expected: false, value: null},
+ {expected: false, value: void 0},
+ {expected: false, value: 0},
+ {expected: false, value: {}},
+ {expected: false, value: []},
+ {expected: true, value: ''}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ minLength: 2
+ },
+ inputs: [
+ {expected: false, value: ''},
+ {expected: false, value: '1'},
+ {expected: true, value: '12'},
+ {expected: true, value: '123'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ maxLength: 2
+ },
+ inputs: [
+ {expected: true, value: ''},
+ {expected: true, value: '1'},
+ {expected: true, value: '12'},
+ {expected: false, value: '123'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: 'test'
+ },
+ inputs: [
+ {expected: false, value: ''},
+ {expected: true, value: 'test'},
+ {expected: false, value: 'TEST'},
+ {expected: true, value: 'ABCtestDEF'},
+ {expected: false, value: 'ABCTESTDEF'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: '^test$'
+ },
+ inputs: [
+ {expected: false, value: ''},
+ {expected: true, value: 'test'},
+ {expected: false, value: 'TEST'},
+ {expected: false, value: 'ABCtestDEF'},
+ {expected: false, value: 'ABCTESTDEF'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: '^test$',
+ patternFlags: 'i'
+ },
+ inputs: [
+ {expected: false, value: ''},
+ {expected: true, value: 'test'},
+ {expected: true, value: 'TEST'},
+ {expected: false, value: 'ABCtestDEF'},
+ {expected: false, value: 'ABCTESTDEF'}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: '*'
+ },
+ inputs: [
+ {expected: false, value: ''}
+ ]
+ },
+ {
+ schema: {
+ type: 'string',
+ pattern: '.',
+ patternFlags: '?'
+ },
+ inputs: [
+ {expected: false, value: ''}
+ ]
+ },
+
+ // Const tests
+ {
+ schema: {
+ const: 32
+ },
+ inputs: [
+ {expected: true, value: 32},
+ {expected: false, value: 0},
+ {expected: false, value: '32'},
+ {expected: false, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+ {
+ schema: {
+ const: '32'
+ },
+ inputs: [
+ {expected: false, value: 32},
+ {expected: false, value: 0},
+ {expected: true, value: '32'},
+ {expected: false, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+ {
+ schema: {
+ const: null
+ },
+ inputs: [
+ {expected: false, value: 32},
+ {expected: false, value: 0},
+ {expected: false, value: '32'},
+ {expected: true, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+ {
+ schema: {
+ const: {a: 'b'}
+ },
+ inputs: [
+ {expected: false, value: 32},
+ {expected: false, value: 0},
+ {expected: false, value: '32'},
+ {expected: false, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+ {
+ schema: {
+ const: [1, 2, 3]
+ },
+ inputs: [
+ {expected: false, value: 32},
+ {expected: false, value: 0},
+ {expected: false, value: '32'},
+ {expected: false, value: null},
+ {expected: false, value: {a: 'b'}},
+ {expected: false, value: [1, 2, 3]}
+ ]
+ },
+
+ // Array contains tests
+ {
+ schema: {
+ type: 'array',
+ contains: {const: 32}
+ },
+ inputs: [
+ {expected: false, value: []},
+ {expected: true, value: [32]},
+ {expected: true, value: [1, 32]},
+ {expected: true, value: [1, 32, 1]},
+ {expected: false, value: [33]},
+ {expected: false, value: [1, 33]},
+ {expected: false, value: [1, 33, 1]}
+ ]
+ },
+
+ // Number limits tests
+ {
+ schema: {
+ type: 'number',
+ minimum: 0
+ },
+ inputs: [
+ {expected: false, value: -1},
+ {expected: true, value: 0},
+ {expected: true, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'number',
+ exclusiveMinimum: 0
+ },
+ inputs: [
+ {expected: false, value: -1},
+ {expected: false, value: 0},
+ {expected: true, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'number',
+ maximum: 0
+ },
+ inputs: [
+ {expected: true, value: -1},
+ {expected: true, value: 0},
+ {expected: false, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'number',
+ exclusiveMaximum: 0
+ },
+ inputs: [
+ {expected: true, value: -1},
+ {expected: false, value: 0},
+ {expected: false, value: 1}
+ ]
+ },
+
+ // Integer limits tests
+ {
+ schema: {
+ type: 'integer',
+ minimum: 0
+ },
+ inputs: [
+ {expected: false, value: -1},
+ {expected: true, value: 0},
+ {expected: true, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer',
+ exclusiveMinimum: 0
+ },
+ inputs: [
+ {expected: false, value: -1},
+ {expected: false, value: 0},
+ {expected: true, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer',
+ maximum: 0
+ },
+ inputs: [
+ {expected: true, value: -1},
+ {expected: true, value: 0},
+ {expected: false, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer',
+ exclusiveMaximum: 0
+ },
+ inputs: [
+ {expected: true, value: -1},
+ {expected: false, value: 0},
+ {expected: false, value: 1}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer',
+ multipleOf: 2
+ },
+ inputs: [
+ {expected: true, value: -2},
+ {expected: false, value: -1},
+ {expected: true, value: 0},
+ {expected: false, value: 1},
+ {expected: true, value: 2}
+ ]
+ },
+
+ // Numeric type tests
+ {
+ schema: {
+ type: 'number'
+ },
+ inputs: [
+ {expected: true, value: 0},
+ {expected: true, value: 0.5},
+ {expected: true, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: false, value: {}}
+ ]
+ },
+ {
+ schema: {
+ type: 'integer'
+ },
+ inputs: [
+ {expected: true, value: 0},
+ {expected: false, value: 0.5},
+ {expected: true, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: false, value: {}}
+ ]
+ },
+
+ // Reference tests
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'number'
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ {expected: true, value: 0},
+ {expected: true, value: 0.5},
+ {expected: true, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: false, value: {}}
+ ]
+ },
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'integer'
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ {expected: true, value: 0},
+ {expected: false, value: 0.5},
+ {expected: true, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: false, value: {}}
+ ]
+ },
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ test: {
+ $ref: '#/definitions/example'
+ }
+ }
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ {expected: false, value: 0},
+ {expected: false, value: 0.5},
+ {expected: false, value: 1},
+ {expected: false, value: '0'},
+ {expected: false, value: null},
+ {expected: false, value: []},
+ {expected: true, value: {}},
+ {expected: false, value: {test: 0}},
+ {expected: false, value: {test: 0.5}},
+ {expected: false, value: {test: 1}},
+ {expected: false, value: {test: '0'}},
+ {expected: false, value: {test: null}},
+ {expected: false, value: {test: []}},
+ {expected: true, value: {test: {}}},
+ {expected: true, value: {test: {test: {}}}},
+ {expected: true, value: {test: {test: {test: {}}}}}
+ ]
+ }
+ ];
+
+ for (const {schema, inputs} of data) {
+ for (const {expected, value} of inputs) {
+ const actual = schemaValidate(schema, value);
+ assert.strictEqual(actual, expected);
+ }
+ }
+}
+
+
+/** */
+function testGetValidValueOrDefault1() {
+ /** @type {{schema: import('json-schema').Schema, inputs: [value: unknown, expected: unknown][]}[]} */
+ const data = [
+ // Test value defaulting on objects with additionalProperties=false
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: false
+ },
+ inputs: [
+ [
+ void 0,
+ {test: 'default'}
+ ],
+ [
+ null,
+ {test: 'default'}
+ ],
+ [
+ 0,
+ {test: 'default'}
+ ],
+ [
+ '',
+ {test: 'default'}
+ ],
+ [
+ [],
+ {test: 'default'}
+ ],
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value'}
+ ]
+ ]
+ },
+
+ // Test value defaulting on objects with additionalProperties=true
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: true
+ },
+ inputs: [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default', test2: 'value2'}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value', test2: 'value2'}
+ ]
+ ]
+ },
+
+ // Test value defaulting on objects with additionalProperties={schema}
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ additionalProperties: {
+ type: 'number',
+ default: 10
+ }
+ },
+ inputs: [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ {test2: 'value2'},
+ {test: 'default', test2: 10}
+ ],
+ [
+ {test: 'value', test2: 'value2'},
+ {test: 'value', test2: 10}
+ ],
+ [
+ {test2: 2},
+ {test: 'default', test2: 2}
+ ],
+ [
+ {test: 'value', test2: 2},
+ {test: 'value', test2: 2}
+ ],
+ [
+ {test: 'value', test2: 2, test3: null},
+ {test: 'value', test2: 2, test3: 10}
+ ],
+ [
+ {test: 'value', test2: 2, test3: void 0},
+ {test: 'value', test2: 2, test3: 10}
+ ]
+ ]
+ },
+
+ // Test value defaulting where hasOwnProperty is false
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ },
+ inputs: [
+ [
+ {},
+ {test: 'default'}
+ ],
+ [
+ {test: 'value'},
+ {test: 'value'}
+ ],
+ [
+ Object.create({test: 'value'}),
+ {test: 'default'}
+ ]
+ ]
+ },
+ {
+ schema: {
+ type: 'object',
+ required: ['toString'],
+ properties: {
+ toString: /** @type {import('json-schema').SchemaObject} */ ({
+ type: 'string',
+ default: 'default'
+ })
+ }
+ },
+ inputs: [
+ [
+ {},
+ {toString: 'default'}
+ ],
+ [
+ {toString: 'value'},
+ {toString: 'value'}
+ ],
+ [
+ Object.create({toString: 'value'}),
+ {toString: 'default'}
+ ]
+ ]
+ },
+
+ // Test enum
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'string',
+ default: 'value1',
+ enum: ['value1', 'value2', 'value3']
+ }
+ }
+ },
+ inputs: [
+ [
+ {test: 'value1'},
+ {test: 'value1'}
+ ],
+ [
+ {test: 'value2'},
+ {test: 'value2'}
+ ],
+ [
+ {test: 'value3'},
+ {test: 'value3'}
+ ],
+ [
+ {test: 'value4'},
+ {test: 'value1'}
+ ]
+ ]
+ },
+
+ // Test valid vs invalid default
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'integer',
+ default: 2,
+ minimum: 1
+ }
+ }
+ },
+ inputs: [
+ [
+ {test: -1},
+ {test: 2}
+ ]
+ ]
+ },
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ properties: {
+ test: {
+ type: 'integer',
+ default: 1,
+ minimum: 2
+ }
+ }
+ },
+ inputs: [
+ [
+ {test: -1},
+ {test: -1}
+ ]
+ ]
+ },
+
+ // Test references
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'number',
+ default: 0
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ [
+ 1,
+ 1
+ ],
+ [
+ null,
+ 0
+ ],
+ [
+ 'test',
+ 0
+ ],
+ [
+ {test: 'value'},
+ 0
+ ]
+ ]
+ },
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ test: {
+ $ref: '#/definitions/example'
+ }
+ }
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ inputs: [
+ [
+ 1,
+ {}
+ ],
+ [
+ null,
+ {}
+ ],
+ [
+ 'test',
+ {}
+ ],
+ [
+ {},
+ {}
+ ],
+ [
+ {test: {}},
+ {test: {}}
+ ],
+ [
+ {test: 'value'},
+ {test: {}}
+ ],
+ [
+ {test: {test: {}}},
+ {test: {test: {}}}
+ ]
+ ]
+ }
+ ];
+
+ for (const {schema, inputs} of data) {
+ for (const [value, expected] of inputs) {
+ const actual = getValidValueOrDefault(schema, value);
+ vm.assert.deepStrictEqual(actual, expected);
+ }
+ }
+}
+
+
+/** */
+function testProxy1() {
+ /** @type {{schema: import('json-schema').Schema, tests: {error: boolean, value?: import('json-schema').Value, action: (value: import('core').SafeAny) => void}[]}[]} */
+ const data = [
+ // Object tests
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ additionalProperties: false,
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ },
+ tests: [
+ {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }},
+ {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }},
+ {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }},
+ {error: true, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }},
+ {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }}
+ ]
+ },
+ {
+ schema: {
+ type: 'object',
+ required: ['test'],
+ additionalProperties: true,
+ properties: {
+ test: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ },
+ tests: [
+ {error: false, value: {test: 'default'}, action: (value) => { value.test = 'string'; }},
+ {error: true, value: {test: 'default'}, action: (value) => { value.test = null; }},
+ {error: true, value: {test: 'default'}, action: (value) => { delete value.test; }},
+ {error: false, value: {test: 'default'}, action: (value) => { value.test2 = 'string'; }},
+ {error: false, value: {test: 'default'}, action: (value) => { delete value.test2; }}
+ ]
+ },
+ {
+ schema: {
+ type: 'object',
+ required: ['test1'],
+ additionalProperties: false,
+ properties: {
+ test1: {
+ type: 'object',
+ required: ['test2'],
+ additionalProperties: false,
+ properties: {
+ test2: {
+ type: 'object',
+ required: ['test3'],
+ additionalProperties: false,
+ properties: {
+ test3: {
+ type: 'string',
+ default: 'default'
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ tests: [
+ {error: false, action: (value) => { value.test1.test2.test3 = 'string'; }},
+ {error: true, action: (value) => { value.test1.test2.test3 = null; }},
+ {error: true, action: (value) => { delete value.test1.test2.test3; }},
+ {error: true, action: (value) => { value.test1.test2 = null; }},
+ {error: true, action: (value) => { value.test1 = null; }},
+ {error: true, action: (value) => { value.test4 = 'string'; }},
+ {error: false, action: (value) => { delete value.test4; }}
+ ]
+ },
+
+ // Array tests
+ {
+ schema: {
+ type: 'array',
+ items: {
+ type: 'string',
+ default: 'default'
+ }
+ },
+ tests: [
+ {error: false, value: ['default'], action: (value) => { value[0] = 'string'; }},
+ {error: true, value: ['default'], action: (value) => { value[0] = null; }},
+ {error: false, value: ['default'], action: (value) => { delete value[0]; }},
+ {error: false, value: ['default'], action: (value) => { value[1] = 'string'; }},
+ {error: false, value: ['default'], action: (value) => {
+ value[1] = 'string';
+ if (value.length !== 2) { throw new Error(`Invalid length; expected=2; actual=${value.length}`); }
+ if (typeof value.push !== 'function') { throw new Error(`Invalid push; expected=function; actual=${typeof value.push}`); }
+ }}
+ ]
+ },
+
+ // Reference tests
+ {
+ schema: {
+ definitions: {
+ example: {
+ type: 'object',
+ additionalProperties: false,
+ properties: {
+ test: {
+ $ref: '#/definitions/example'
+ }
+ }
+ }
+ },
+ $ref: '#/definitions/example'
+ },
+ tests: [
+ {error: false, value: {}, action: (value) => { value.test = {}; }},
+ {error: false, value: {}, action: (value) => { value.test = {}; value.test.test = {}; }},
+ {error: false, value: {}, action: (value) => { value.test = {test: {}}; }},
+ {error: true, value: {}, action: (value) => { value.test = null; }},
+ {error: true, value: {}, action: (value) => { value.test = 'string'; }},
+ {error: true, value: {}, action: (value) => { value.test = {}; value.test.test = 'string'; }},
+ {error: true, value: {}, action: (value) => { value.test = {test: 'string'}; }}
+ ]
+ }
+ ];
+
+ for (const {schema, tests} of data) {
+ for (let {error, value, action} of tests) {
+ if (typeof value === 'undefined') { value = getValidValueOrDefault(schema, void 0); }
+ value = clone(value);
+ assert.ok(schemaValidate(schema, value));
+ const valueProxy = createProxy(schema, value);
+ if (error) {
+ assert.throws(() => action(valueProxy));
+ } else {
+ assert.doesNotThrow(() => action(valueProxy));
+ }
+ }
+ }
+}
+
+
+/** */
+function main() {
+ testValidate1();
+ testValidate2();
+ testGetValidValueOrDefault1();
+ testProxy1();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-manifest.js b/test/test-manifest.js
new file mode 100644
index 00000000..32a498e1
--- /dev/null
+++ b/test/test-manifest.js
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+const {testMain} = require('../dev/util');
+const {ManifestUtil} = require('../dev/manifest-util');
+
+
+/**
+ * @returns {string}
+ */
+function loadManifestString() {
+ const manifestPath = path.join(__dirname, '..', 'ext', 'manifest.json');
+ return fs.readFileSync(manifestPath, {encoding: 'utf8'});
+}
+
+/** */
+function validateManifest() {
+ const manifestUtil = new ManifestUtil();
+ const manifest1 = loadManifestString();
+ const manifest2 = ManifestUtil.createManifestString(manifestUtil.getManifest());
+ assert.strictEqual(manifest1, manifest2, 'Manifest data does not match.');
+}
+
+
+/** */
+function main() {
+ validateManifest();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-object-property-accessor.js b/test/test-object-property-accessor.js
new file mode 100644
index 00000000..8826d6a9
--- /dev/null
+++ b/test/test-object-property-accessor.js
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+const vm = new VM({});
+vm.execute('js/general/object-property-accessor.js');
+/** @type {typeof ObjectPropertyAccessor} */
+const ObjectPropertyAccessor2 = vm.getSingle('ObjectPropertyAccessor');
+
+
+/**
+ * @returns {import('core').UnknownObject}
+ */
+function createTestObject() {
+ return {
+ 0: null,
+ value1: {
+ value2: {},
+ value3: [],
+ value4: null
+ },
+ value5: [
+ {},
+ [],
+ null
+ ]
+ };
+}
+
+
+/** */
+function testGet1() {
+ /** @type {[pathArray: (string|number)[], getExpected: (object: import('core').SafeAny) => unknown][]} */
+ const data = [
+ [[], (object) => object],
+ [['0'], (object) => object['0']],
+ [['value1'], (object) => object.value1],
+ [['value1', 'value2'], (object) => object.value1.value2],
+ [['value1', 'value3'], (object) => object.value1.value3],
+ [['value1', 'value4'], (object) => object.value1.value4],
+ [['value5'], (object) => object.value5],
+ [['value5', 0], (object) => object.value5[0]],
+ [['value5', 1], (object) => object.value5[1]],
+ [['value5', 2], (object) => object.value5[2]]
+ ];
+
+ for (const [pathArray, getExpected] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor2(object);
+ const expected = getExpected(object);
+
+ assert.strictEqual(accessor.get(pathArray), expected);
+ }
+}
+
+/** */
+function testGet2() {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor2(object);
+
+ /** @type {[pathArray: (string|number)[], message: string][]} */
+ const data = [
+ [[0], 'Invalid path: [0]'],
+ [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
+ [['invalid'], 'Invalid path: invalid'],
+ [['value1', 'invalid'], 'Invalid path: value1.invalid'],
+ [['value1', 'value2', 'invalid'], 'Invalid path: value1.value2.invalid'],
+ [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
+ [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
+ [['value1', 'value3', 0], 'Invalid path: value1.value3[0]'],
+ [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
+ [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
+ [['value5', 'length'], 'Invalid path: value5.length'],
+ [['value5', 0, 'invalid'], 'Invalid path: value5[0].invalid'],
+ [['value5', 0, 0], 'Invalid path: value5[0][0]'],
+ [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
+ [['value5', 1, 0], 'Invalid path: value5[1][0]'],
+ [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
+ [['value5', 2, 0], 'Invalid path: value5[2][0]'],
+ [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
+ [['value5', 2.5], 'Invalid index']
+ ];
+
+ for (const [pathArray, message] of data) {
+ assert.throws(() => accessor.get(pathArray), {message});
+ }
+}
+
+
+/** */
+function testSet1() {
+ const testValue = {};
+ /** @type {(string|number)[][]} */
+ const data = [
+ ['0'],
+ ['value1', 'value2'],
+ ['value1', 'value3'],
+ ['value1', 'value4'],
+ ['value1'],
+ ['value5', 0],
+ ['value5', 1],
+ ['value5', 2],
+ ['value5']
+ ];
+
+ for (const pathArray of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor2(object);
+
+ accessor.set(pathArray, testValue);
+ assert.strictEqual(accessor.get(pathArray), testValue);
+ }
+}
+
+/** */
+function testSet2() {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor2(object);
+
+ const testValue = {};
+ /** @type {[pathArray: (string|number)[], message: string][]} */
+ const data = [
+ [[], 'Invalid path'],
+ [[0], 'Invalid path: [0]'],
+ [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
+ [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
+ [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
+ [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
+ [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
+ [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
+ [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
+ [['value5', 2, 0], 'Invalid path: value5[2][0]'],
+ [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
+ [['value5', 2.5], 'Invalid index']
+ ];
+
+ for (const [pathArray, message] of data) {
+ assert.throws(() => accessor.set(pathArray, testValue), {message});
+ }
+}
+
+
+/** */
+function testDelete1() {
+ /**
+ * @param {unknown} object
+ * @param {string} property
+ * @returns {boolean}
+ */
+ const hasOwn = (object, property) => Object.prototype.hasOwnProperty.call(object, property);
+
+ /** @type {[pathArray: (string|number)[], validate: (object: import('core').SafeAny) => boolean][]} */
+ const data = [
+ [['0'], (object) => !hasOwn(object, '0')],
+ [['value1', 'value2'], (object) => !hasOwn(object.value1, 'value2')],
+ [['value1', 'value3'], (object) => !hasOwn(object.value1, 'value3')],
+ [['value1', 'value4'], (object) => !hasOwn(object.value1, 'value4')],
+ [['value1'], (object) => !hasOwn(object, 'value1')],
+ [['value5'], (object) => !hasOwn(object, 'value5')]
+ ];
+
+ for (const [pathArray, validate] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor2(object);
+
+ accessor.delete(pathArray);
+ assert.ok(validate(object));
+ }
+}
+
+/** */
+function testDelete2() {
+ /** @type {[pathArray: (string|number)[], message: string][]} */
+ const data = [
+ [[], 'Invalid path'],
+ [[0], 'Invalid path: [0]'],
+ [['0', 'invalid'], 'Invalid path: ["0"].invalid'],
+ [['value1', 'value2', 0], 'Invalid path: value1.value2[0]'],
+ [['value1', 'value3', 'invalid'], 'Invalid path: value1.value3.invalid'],
+ [['value1', 'value4', 'invalid'], 'Invalid path: value1.value4.invalid'],
+ [['value1', 'value4', 0], 'Invalid path: value1.value4[0]'],
+ [['value5', 1, 'invalid'], 'Invalid path: value5[1].invalid'],
+ [['value5', 2, 'invalid'], 'Invalid path: value5[2].invalid'],
+ [['value5', 2, 0], 'Invalid path: value5[2][0]'],
+ [['value5', 2, 0, 'invalid'], 'Invalid path: value5[2][0]'],
+ [['value5', 2.5], 'Invalid index'],
+ [['value5', 0], 'Invalid type'],
+ [['value5', 1], 'Invalid type'],
+ [['value5', 2], 'Invalid type']
+ ];
+
+ for (const [pathArray, message] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor2(object);
+
+ assert.throws(() => accessor.delete(pathArray), {message});
+ }
+}
+
+
+/** */
+function testSwap1() {
+ /** @type {[pathArray: (string|number)[], compareValues: boolean][]} */
+ const data = [
+ [['0'], true],
+ [['value1', 'value2'], true],
+ [['value1', 'value3'], true],
+ [['value1', 'value4'], true],
+ [['value1'], false],
+ [['value5', 0], true],
+ [['value5', 1], true],
+ [['value5', 2], true],
+ [['value5'], false]
+ ];
+
+ for (const [pathArray1, compareValues1] of data) {
+ for (const [pathArray2, compareValues2] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor2(object);
+
+ const value1a = accessor.get(pathArray1);
+ const value2a = accessor.get(pathArray2);
+
+ accessor.swap(pathArray1, pathArray2);
+
+ if (!compareValues1 || !compareValues2) { continue; }
+
+ const value1b = accessor.get(pathArray1);
+ const value2b = accessor.get(pathArray2);
+
+ assert.deepStrictEqual(value1a, value2b);
+ assert.deepStrictEqual(value2a, value1b);
+ }
+ }
+}
+
+/** */
+function testSwap2() {
+ /** @type {[pathArray1: (string|number)[], pathArray2: (string|number)[], checkRevert: boolean, message: string][]} */
+ const data = [
+ [[], [], false, 'Invalid path 1'],
+ [['0'], [], false, 'Invalid path 2'],
+ [[], ['0'], false, 'Invalid path 1'],
+ [[0], ['0'], false, 'Invalid path 1: [0]'],
+ [['0'], [0], false, 'Invalid path 2: [0]']
+ ];
+
+ for (const [pathArray1, pathArray2, checkRevert, message] of data) {
+ const object = createTestObject();
+ const accessor = new ObjectPropertyAccessor2(object);
+
+ let value1a;
+ let value2a;
+ if (checkRevert) {
+ try {
+ value1a = accessor.get(pathArray1);
+ value2a = accessor.get(pathArray2);
+ } catch (e) {
+ // NOP
+ }
+ }
+
+ assert.throws(() => accessor.swap(pathArray1, pathArray2), {message});
+
+ if (!checkRevert) { continue; }
+
+ const value1b = accessor.get(pathArray1);
+ const value2b = accessor.get(pathArray2);
+
+ assert.deepStrictEqual(value1a, value1b);
+ assert.deepStrictEqual(value2a, value2b);
+ }
+}
+
+
+/** */
+function testGetPathString1() {
+ /** @type {[pathArray: (string|number)[], expected: string][]} */
+ const data = [
+ [[], ''],
+ [[0], '[0]'],
+ [['escape\\'], '["escape\\\\"]'],
+ [['\'quote\''], '["\'quote\'"]'],
+ [['"quote"'], '["\\"quote\\""]'],
+ [['part1', 'part2'], 'part1.part2'],
+ [['part1', 'part2', 3], 'part1.part2[3]'],
+ [['part1', 'part2', '3'], 'part1.part2["3"]'],
+ [['part1', 'part2', '3part'], 'part1.part2["3part"]'],
+ [['part1', 'part2', '3part', 'part4'], 'part1.part2["3part"].part4'],
+ [['part1', 'part2', '3part', '4part'], 'part1.part2["3part"]["4part"]']
+ ];
+
+ for (const [pathArray, expected] of data) {
+ assert.strictEqual(ObjectPropertyAccessor2.getPathString(pathArray), expected);
+ }
+}
+
+/** */
+function testGetPathString2() {
+ /** @type {[pathArray: unknown[], message: string][]} */
+ const data = [
+ [[1.5], 'Invalid index'],
+ [[null], 'Invalid type: object']
+ ];
+
+ for (const [pathArray, message] of data) {
+ // @ts-ignore - Throwing is expected
+ assert.throws(() => ObjectPropertyAccessor2.getPathString(pathArray), {message});
+ }
+}
+
+
+/** */
+function testGetPathArray1() {
+ /** @type {[pathString: string, pathArray: (string|number)[]][]} */
+ const data = [
+ ['', []],
+ ['[0]', [0]],
+ ['["escape\\\\"]', ['escape\\']],
+ ['["\'quote\'"]', ['\'quote\'']],
+ ['["\\"quote\\""]', ['"quote"']],
+ ['part1.part2', ['part1', 'part2']],
+ ['part1.part2[3]', ['part1', 'part2', 3]],
+ ['part1.part2["3"]', ['part1', 'part2', '3']],
+ ['part1.part2[\'3\']', ['part1', 'part2', '3']],
+ ['part1.part2["3part"]', ['part1', 'part2', '3part']],
+ ['part1.part2[\'3part\']', ['part1', 'part2', '3part']],
+ ['part1.part2["3part"].part4', ['part1', 'part2', '3part', 'part4']],
+ ['part1.part2[\'3part\'].part4', ['part1', 'part2', '3part', 'part4']],
+ ['part1.part2["3part"]["4part"]', ['part1', 'part2', '3part', '4part']],
+ ['part1.part2[\'3part\'][\'4part\']', ['part1', 'part2', '3part', '4part']]
+ ];
+
+ for (const [pathString, expected] of data) {
+ // @ts-ignore
+ vm.assert.deepStrictEqual(ObjectPropertyAccessor2.getPathArray(pathString), expected);
+ }
+}
+
+/** */
+function testGetPathArray2() {
+ /** @type {[pathString: string, message: string][]} */
+ const data = [
+ ['?', 'Unexpected character: ?'],
+ ['.', 'Unexpected character: .'],
+ ['0', 'Unexpected character: 0'],
+ ['part1.[0]', 'Unexpected character: ['],
+ ['part1?', 'Unexpected character: ?'],
+ ['[part1]', 'Unexpected character: p'],
+ ['[0a]', 'Unexpected character: a'],
+ ['["part1"x]', 'Unexpected character: x'],
+ ['[\'part1\'x]', 'Unexpected character: x'],
+ ['["part1"]x', 'Unexpected character: x'],
+ ['[\'part1\']x', 'Unexpected character: x'],
+ ['part1..part2', 'Unexpected character: .'],
+
+ ['[', 'Path not terminated correctly'],
+ ['part1.', 'Path not terminated correctly'],
+ ['part1[', 'Path not terminated correctly'],
+ ['part1["', 'Path not terminated correctly'],
+ ['part1[\'', 'Path not terminated correctly'],
+ ['part1[""', 'Path not terminated correctly'],
+ ['part1[\'\'', 'Path not terminated correctly'],
+ ['part1[0', 'Path not terminated correctly'],
+ ['part1[0].', 'Path not terminated correctly']
+ ];
+
+ for (const [pathString, message] of data) {
+ assert.throws(() => ObjectPropertyAccessor2.getPathArray(pathString), {message});
+ }
+}
+
+
+/** */
+function testHasProperty() {
+ /** @type {[object: unknown, property: unknown, expected: boolean][]} */
+ const data = [
+ [{}, 'invalid', false],
+ [{}, 0, false],
+ [{valid: 0}, 'valid', true],
+ [{null: 0}, null, false],
+ [[], 'invalid', false],
+ [[], 0, false],
+ [[0], 0, true],
+ [[0], null, false],
+ ['string', 0, false],
+ ['string', 'length', false],
+ ['string', null, false]
+ ];
+
+ for (const [object, property, expected] of data) {
+ // @ts-ignore
+ assert.strictEqual(ObjectPropertyAccessor2.hasProperty(object, property), expected);
+ }
+}
+
+/** */
+function testIsValidPropertyType() {
+ /** @type {[object: unknown, property: unknown, expected: boolean][]} */
+ const data = [
+ [{}, 'invalid', true],
+ [{}, 0, false],
+ [{valid: 0}, 'valid', true],
+ [{null: 0}, null, false],
+ [[], 'invalid', false],
+ [[], 0, true],
+ [[0], 0, true],
+ [[0], null, false],
+ ['string', 0, false],
+ ['string', 'length', false],
+ ['string', null, false]
+ ];
+
+ for (const [object, property, expected] of data) {
+ // @ts-ignore
+ assert.strictEqual(ObjectPropertyAccessor2.isValidPropertyType(object, property), expected);
+ }
+}
+
+
+/** */
+function main() {
+ testGet1();
+ testGet2();
+ testSet1();
+ testSet2();
+ testDelete1();
+ testDelete2();
+ testSwap1();
+ testSwap2();
+ testGetPathString1();
+ testGetPathString2();
+ testGetPathArray1();
+ testGetPathArray2();
+ testHasProperty();
+ testIsValidPropertyType();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-profile-conditions-util.js b/test/test-profile-conditions-util.js
new file mode 100644
index 00000000..2e6f751f
--- /dev/null
+++ b/test/test-profile-conditions-util.js
@@ -0,0 +1,1136 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+
+const vm = new VM({});
+vm.execute([
+ 'js/core.js',
+ 'js/core/extension-error.js',
+ 'js/general/cache-map.js',
+ 'js/data/json-schema.js',
+ 'js/background/profile-conditions-util.js'
+]);
+/** @type {typeof ProfileConditionsUtil} */
+const ProfileConditionsUtil2 = vm.getSingle('ProfileConditionsUtil');
+
+
+/** */
+function testNormalizeContext() {
+ /** @type {{context: import('settings').OptionsContext, expected: import('profile-conditions-util').NormalizedOptionsContext}[]} */
+ const data = [
+ // Empty
+ {
+ context: {index: 0},
+ expected: {index: 0, flags: []}
+ },
+
+ // Domain normalization
+ {
+ context: {depth: 0, url: ''},
+ expected: {depth: 0, url: '', flags: []}
+ },
+ {
+ context: {depth: 0, url: 'http://example.com/'},
+ expected: {depth: 0, url: 'http://example.com/', domain: 'example.com', flags: []}
+ },
+ {
+ context: {depth: 0, url: 'http://example.com:1234/'},
+ expected: {depth: 0, url: 'http://example.com:1234/', domain: 'example.com', flags: []}
+ },
+ {
+ context: {depth: 0, url: 'http://user@example.com:1234/'},
+ expected: {depth: 0, url: 'http://user@example.com:1234/', domain: 'example.com', flags: []}
+ }
+ ];
+
+ for (const {context, expected} of data) {
+ const profileConditionsUtil = new ProfileConditionsUtil2();
+ const actual = profileConditionsUtil.normalizeContext(context);
+ vm.assert.deepStrictEqual(actual, expected);
+ }
+}
+
+/** */
+function testSchemas() {
+ /** @type {{conditionGroups: import('settings').ProfileConditionGroup[], expectedSchema?: import('json-schema').Schema, inputs?: {expected: boolean, context: import('settings').OptionsContext}[]}[]} */
+ const data = [
+ // Empty
+ {
+ conditionGroups: [],
+ expectedSchema: {},
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {conditions: []}
+ ],
+ expectedSchema: {},
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {conditions: []},
+ {conditions: []}
+ ],
+ expectedSchema: {},
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}}
+ ]
+ },
+
+ // popupLevel tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'equal',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {const: 0}
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'notEqual',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: {
+ anyOf: [
+ {
+ properties: {
+ depth: {const: 0}
+ },
+ required: ['depth']
+ }
+ ]
+ }
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'lessThan',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMaximum: 0
+ }
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThan',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMinimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'lessThanOrEqual',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {
+ type: 'number',
+ maximum: 0
+ }
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThanOrEqual',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ depth: {
+ type: 'number',
+ minimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}}
+ ]
+ },
+
+ // url tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'url',
+ operator: 'matchDomain',
+ value: 'example.com'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ domain: {
+ oneOf: [
+ {const: 'example.com'}
+ ]
+ }
+ },
+ required: ['domain']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 0, url: 'http://example1.com/'}},
+ {expected: false, context: {depth: 0, url: 'http://example2.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}},
+ {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'url',
+ operator: 'matchDomain',
+ value: 'example.com, example1.com, example2.com'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ domain: {
+ oneOf: [
+ {const: 'example.com'},
+ {const: 'example1.com'},
+ {const: 'example2.com'}
+ ]
+ }
+ },
+ required: ['domain']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example1.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example2.com/'}},
+ {expected: false, context: {depth: 0, url: 'http://example3.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com:1234/'}},
+ {expected: true, context: {depth: 0, url: 'http://user@example.com:1234/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'url',
+ operator: 'matchRegExp',
+ value: '^http://example\\d?\\.com/[\\w\\W]*$'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ url: {
+ type: 'string',
+ pattern: '^http://example\\d?\\.com/[\\w\\W]*$',
+ patternFlags: 'i'
+ }
+ },
+ required: ['url']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example1.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example2.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example3.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/example'}},
+ {expected: false, context: {depth: 0, url: 'http://example.com:1234/'}},
+ {expected: false, context: {depth: 0, url: 'http://user@example.com:1234/'}},
+ {expected: false, context: {depth: 0, url: 'http://example-1.com/'}}
+ ]
+ },
+
+ // modifierKeys tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'are',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ maxItems: 0,
+ minItems: 0
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'are',
+ value: 'alt, shift'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ maxItems: 2,
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'alt'}},
+ {contains: {const: 'shift'}}
+ ]
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'areNot',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: {
+ anyOf: [
+ {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ maxItems: 0,
+ minItems: 0
+ }
+ },
+ required: ['modifierKeys']
+ }
+ ]
+ }
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'areNot',
+ value: 'alt, shift'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: {
+ anyOf: [
+ {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ maxItems: 2,
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'alt'}},
+ {contains: {const: 'shift'}}
+ ]
+ }
+ },
+ required: ['modifierKeys']
+ }
+ ]
+ }
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'include',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ minItems: 0
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'include',
+ value: 'alt, shift'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'alt'}},
+ {contains: {const: 'shift'}}
+ ]
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'notInclude',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array'
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'modifierKeys',
+ operator: 'notInclude',
+ value: 'alt, shift'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ properties: {
+ modifierKeys: {
+ type: 'array',
+ not: {
+ anyOf: [
+ {contains: {const: 'alt'}},
+ {contains: {const: 'shift'}}
+ ]
+ }
+ }
+ },
+ required: ['modifierKeys']
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: 'http://example.com/', modifierKeys: []}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift']}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/', modifierKeys: ['alt', 'shift', 'ctrl']}}
+ ]
+ },
+
+ // flags tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'are',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ maxItems: 0,
+ minItems: 0
+ }
+ }
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: ''}},
+ {expected: true, context: {depth: 0, url: '', flags: []}},
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'are',
+ value: 'clipboard, test2'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ maxItems: 2,
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'clipboard'}},
+ {contains: {const: 'test2'}}
+ ]
+ }
+ }
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: ''}},
+ {expected: false, context: {depth: 0, url: '', flags: []}},
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'areNot',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: {
+ anyOf: [
+ {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ maxItems: 0,
+ minItems: 0
+ }
+ }
+ }
+ ]
+ }
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: ''}},
+ {expected: false, context: {depth: 0, url: '', flags: []}},
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'areNot',
+ value: 'clipboard, test2'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ not: {
+ anyOf: [
+ {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ maxItems: 2,
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'clipboard'}},
+ {contains: {const: 'test2'}}
+ ]
+ }
+ }
+ }
+ ]
+ }
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: ''}},
+ {expected: true, context: {depth: 0, url: '', flags: []}},
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'include',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ minItems: 0
+ }
+ }
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: ''}},
+ {expected: true, context: {depth: 0, url: '', flags: []}},
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'include',
+ value: 'clipboard, test2'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ minItems: 2,
+ allOf: [
+ {contains: {const: 'clipboard'}},
+ {contains: {const: 'test2'}}
+ ]
+ }
+ }
+ },
+ inputs: [
+ {expected: false, context: {depth: 0, url: ''}},
+ {expected: false, context: {depth: 0, url: '', flags: []}},
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'notInclude',
+ value: ''
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array'
+ }
+ }
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: ''}},
+ {expected: true, context: {depth: 0, url: '', flags: []}},
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: true, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'flags',
+ operator: 'notInclude',
+ value: 'clipboard, test2'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ required: ['flags'],
+ properties: {
+ flags: {
+ type: 'array',
+ not: {
+ anyOf: [
+ {contains: {const: 'clipboard'}},
+ {contains: {const: 'test2'}}
+ ]
+ }
+ }
+ }
+ },
+ inputs: [
+ {expected: true, context: {depth: 0, url: ''}},
+ {expected: true, context: {depth: 0, url: '', flags: []}},
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2']}},
+ // @ts-ignore - Ignore type for string flag for testing purposes
+ {expected: false, context: {depth: 0, url: '', flags: ['clipboard', 'test2', 'test3']}}
+ ]
+ },
+
+ // Multiple conditions tests
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThan',
+ value: '0'
+ },
+ {
+ type: 'popupLevel',
+ operator: 'lessThan',
+ value: '3'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ allOf: [
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMinimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMaximum: 3
+ }
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {depth: -2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 3, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThan',
+ value: '0'
+ },
+ {
+ type: 'popupLevel',
+ operator: 'lessThan',
+ value: '3'
+ }
+ ]
+ },
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'equal',
+ value: '0'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ anyOf: [
+ {
+ allOf: [
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMinimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMaximum: 3
+ }
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ {
+ properties: {
+ depth: {const: 0}
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {depth: -2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: -1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 3, url: 'http://example.com/'}}
+ ]
+ },
+ {
+ conditionGroups: [
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'greaterThan',
+ value: '0'
+ },
+ {
+ type: 'popupLevel',
+ operator: 'lessThan',
+ value: '3'
+ }
+ ]
+ },
+ {
+ conditions: [
+ {
+ type: 'popupLevel',
+ operator: 'lessThanOrEqual',
+ value: '0'
+ },
+ {
+ type: 'popupLevel',
+ operator: 'greaterThanOrEqual',
+ value: '-1'
+ }
+ ]
+ }
+ ],
+ expectedSchema: {
+ anyOf: [
+ {
+ allOf: [
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMinimum: 0
+ }
+ },
+ required: ['depth']
+ },
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ exclusiveMaximum: 3
+ }
+ },
+ required: ['depth']
+ }
+ ]
+ },
+ {
+ allOf: [
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ maximum: 0
+ }
+ },
+ required: ['depth']
+ },
+ {
+ properties: {
+ depth: {
+ type: 'number',
+ minimum: -1
+ }
+ },
+ required: ['depth']
+ }
+ ]
+ }
+ ]
+ },
+ inputs: [
+ {expected: false, context: {depth: -2, url: 'http://example.com/'}},
+ {expected: true, context: {depth: -1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 0, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 1, url: 'http://example.com/'}},
+ {expected: true, context: {depth: 2, url: 'http://example.com/'}},
+ {expected: false, context: {depth: 3, url: 'http://example.com/'}}
+ ]
+ }
+ ];
+
+ for (const {conditionGroups, expectedSchema, inputs} of data) {
+ const profileConditionsUtil = new ProfileConditionsUtil2();
+ const schema = profileConditionsUtil.createSchema(conditionGroups);
+ if (typeof expectedSchema !== 'undefined') {
+ vm.assert.deepStrictEqual(schema.schema, expectedSchema);
+ }
+ if (Array.isArray(inputs)) {
+ for (const {expected, context} of inputs) {
+ const normalizedContext = profileConditionsUtil.normalizeContext(context);
+ const actual = schema.isValid(normalizedContext);
+ assert.strictEqual(actual, expected);
+ }
+ }
+ }
+}
+
+
+/** */
+function main() {
+ testNormalizeContext();
+ testSchemas();
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-text-source-map.js b/test/test-text-source-map.js
new file mode 100644
index 00000000..834a3d07
--- /dev/null
+++ b/test/test-text-source-map.js
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 {testMain} = require('../dev/util');
+const {VM} = require('../dev/vm');
+
+const vm = new VM();
+vm.execute(['js/general/text-source-map.js']);
+/** @type {typeof TextSourceMap} */
+const TextSourceMap2 = vm.getSingle('TextSourceMap');
+
+
+/** */
+function testSource() {
+ const data = [
+ ['source1'],
+ ['source2'],
+ ['source3']
+ ];
+
+ for (const [source] of data) {
+ const sourceMap = new TextSourceMap2(source);
+ assert.strictEqual(source, sourceMap.source);
+ }
+}
+
+/** */
+function testEquals() {
+ /** @type {[args1: [source1: string, mapping1: ?(number[])], args2: [source2: string, mapping2: ?(number[])], expectedEquals: boolean][]} */
+ 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 TextSourceMap2(source1, mapping1);
+ const sourceMap2 = new TextSourceMap2(source2, mapping2);
+ assert.ok(sourceMap1.equals(sourceMap1));
+ assert.ok(sourceMap2.equals(sourceMap2));
+ assert.strictEqual(sourceMap1.equals(sourceMap2), expectedEquals);
+ }
+}
+
+/** */
+function testGetSourceLength() {
+ /** @type {[args: [source: string, mapping: number[]], finalLength: number, expectedValue: number][]} */
+ 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 TextSourceMap2(source, mapping);
+ assert.strictEqual(sourceMap.getSourceLength(finalLength), expectedValue);
+ }
+}
+
+/** */
+function testCombineInsert() {
+ /** @type {[args: [source: string, mapping: ?(number[])], expectedArgs: [expectedSource: string, expectedMapping: ?(number[])], operations: [operation: string, arg1: number, arg2: number][]][]} */
+ 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 TextSourceMap2(source, mapping);
+ const expectedSourceMap = new TextSourceMap2(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) { testMain(main); }
diff --git a/test/test-translator.js b/test/test-translator.js
new file mode 100644
index 00000000..0c84e0be
--- /dev/null
+++ b/test/test-translator.js
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 fs = require('fs');
+const path = require('path');
+const assert = require('assert');
+const {testMain} = require('../dev/util');
+const {TranslatorVM} = require('../dev/translator-vm');
+
+
+/**
+ * @template T
+ * @param {T} value
+ * @returns {T}
+ */
+function clone(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+
+/** */
+async function main() {
+ const write = (process.argv[2] === '--write');
+
+ const translatorVM = new TranslatorVM();
+ const dictionaryDirectory = path.join(__dirname, 'data', 'dictionaries', 'valid-dictionary1');
+ await translatorVM.prepare(dictionaryDirectory, 'Test Dictionary 2');
+
+ const testInputsFilePath = path.join(__dirname, 'data', 'translator-test-inputs.json');
+ const {optionsPresets, tests} = JSON.parse(fs.readFileSync(testInputsFilePath, {encoding: 'utf8'}));
+
+ const testResults1FilePath = path.join(__dirname, 'data', 'translator-test-results.json');
+ const expectedResults1 = JSON.parse(fs.readFileSync(testResults1FilePath, {encoding: 'utf8'}));
+ const actualResults1 = [];
+
+ const testResults2FilePath = path.join(__dirname, 'data', 'translator-test-results-note-data1.json');
+ const expectedResults2 = JSON.parse(fs.readFileSync(testResults2FilePath, {encoding: 'utf8'}));
+ const actualResults2 = [];
+
+ for (let i = 0, ii = tests.length; i < ii; ++i) {
+ const test = tests[i];
+ const expected1 = expectedResults1[i];
+ const expected2 = expectedResults2[i];
+ switch (test.func) {
+ case 'findTerms':
+ {
+ const {name, mode, text} = test;
+ /** @type {import('translation').FindTermsOptions} */
+ const options = translatorVM.buildOptions(optionsPresets, test.options);
+ const {dictionaryEntries, originalTextLength} = clone(await translatorVM.translator.findTerms(mode, text, options));
+ const noteDataList = mode !== 'simple' ? clone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(clone(dictionaryEntry), mode))) : null;
+ actualResults1.push({name, originalTextLength, dictionaryEntries});
+ actualResults2.push({name, noteDataList});
+ if (!write) {
+ assert.deepStrictEqual(originalTextLength, expected1.originalTextLength);
+ assert.deepStrictEqual(dictionaryEntries, expected1.dictionaryEntries);
+ assert.deepStrictEqual(noteDataList, expected2.noteDataList);
+ }
+ }
+ break;
+ case 'findKanji':
+ {
+ const {name, text} = test;
+ /** @type {import('translation').FindKanjiOptions} */
+ const options = translatorVM.buildOptions(optionsPresets, test.options);
+ const dictionaryEntries = clone(await translatorVM.translator.findKanji(text, options));
+ const noteDataList = clone(dictionaryEntries.map((dictionaryEntry) => translatorVM.createTestAnkiNoteData(clone(dictionaryEntry), 'split')));
+ actualResults1.push({name, dictionaryEntries});
+ actualResults2.push({name, noteDataList});
+ if (!write) {
+ assert.deepStrictEqual(dictionaryEntries, expected1.dictionaryEntries);
+ assert.deepStrictEqual(noteDataList, expected2.noteDataList);
+ }
+ }
+ break;
+ }
+ }
+
+ if (write) {
+ // Use 2 indent instead of 4 to save a bit of file size
+ fs.writeFileSync(testResults1FilePath, JSON.stringify(actualResults1, null, 2), {encoding: 'utf8'});
+ fs.writeFileSync(testResults2FilePath, JSON.stringify(actualResults2, null, 2), {encoding: 'utf8'});
+ }
+}
+
+
+if (require.main === module) { testMain(main); }
diff --git a/test/test-workers.js b/test/test-workers.js
new file mode 100644
index 00000000..3de7ac48
--- /dev/null
+++ b/test/test-workers.js
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 Yomitan Authors
+ * Copyright (C) 2020-2022 Yomichan Authors
+ *
+ * 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 fs = require('fs');
+const path = require('path');
+const {JSDOM} = require('jsdom');
+const {VM} = require('../dev/vm');
+const assert = require('assert');
+
+
+class StubClass {
+ /** */
+ prepare() {
+ // NOP
+ }
+}
+
+
+/**
+ * @returns {import('core').SafeAny}
+ */
+function loadEslint() {
+ return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '.eslintrc.json'), {encoding: 'utf8'}));
+}
+
+/**
+ * @param {string[]} scriptPaths
+ * @returns {string[]}
+ */
+function filterScriptPaths(scriptPaths) {
+ const extDirName = 'ext';
+ return scriptPaths.filter((src) => !src.startsWith('/lib/')).map((src) => `${extDirName}${src}`);
+}
+
+/**
+ * @param {string} fileName
+ * @returns {string[]}
+ */
+function getAllHtmlScriptPaths(fileName) {
+ const domSource = fs.readFileSync(fileName, {encoding: 'utf8'});
+ const dom = new JSDOM(domSource);
+ const {window} = dom;
+ const {document} = window;
+ try {
+ const scripts = document.querySelectorAll('script');
+ return [...scripts].map(({src}) => src);
+ } finally {
+ window.close();
+ }
+}
+
+/**
+ * @param {string[]} scripts
+ */
+function convertBackgroundScriptsToServiceWorkerScripts(scripts) {
+ // Use parse5-based SimpleDOMParser
+ scripts.splice(0, 0, '/lib/parse5.js');
+ const index = scripts.indexOf('/js/dom/native-simple-dom-parser.js');
+ assert.ok(index >= 0);
+ scripts[index] = '/js/dom/simple-dom-parser.js';
+}
+
+/**
+ * @param {string} scriptPath
+ * @param {import('core').UnknownObject} fields
+ * @returns {string[]}
+ */
+function getImportedScripts(scriptPath, fields) {
+ /** @type {string[]} */
+ const importedScripts = [];
+
+ /**
+ * @param {...string} scripts
+ */
+ const importScripts = (...scripts) => {
+ importedScripts.push(...scripts);
+ };
+
+ const vm = new VM(Object.assign({importScripts}, fields));
+ vm.context.self = vm.context;
+ vm.execute([scriptPath]);
+
+ return importedScripts;
+}
+
+/** */
+function testServiceWorker() {
+ // Verify that sw.js scripts match background.html scripts
+ const extDir = path.join(__dirname, '..', 'ext');
+ const scripts = getAllHtmlScriptPaths(path.join(extDir, 'background.html'));
+ convertBackgroundScriptsToServiceWorkerScripts(scripts);
+ const importedScripts = getImportedScripts('sw.js', {});
+ assert.deepStrictEqual(scripts, importedScripts);
+
+ // Verify that eslint config lists files correctly
+ const expectedSwRulesFiles = filterScriptPaths(scripts);
+ const eslintConfig = loadEslint();
+ const swRules = /** @type {import('core').SafeAny[]} */ (eslintConfig.overrides).find((item) => (
+ typeof item.env === 'object' &&
+ item.env !== null &&
+ item.env.serviceworker === true
+ ));
+ assert.ok(typeof swRules !== 'undefined');
+ assert.ok(Array.isArray(swRules.files));
+ assert.deepStrictEqual(swRules.files, expectedSwRulesFiles);
+}
+
+/** */
+function testWorkers() {
+ testWorker(
+ 'js/language/dictionary-worker-main.js',
+ {DictionaryWorkerHandler: StubClass}
+ );
+}
+
+/**
+ * @param {string} scriptPath
+ * @param {import('core').UnknownObject} fields
+ */
+function testWorker(scriptPath, fields) {
+ // Get script paths
+ const scripts = getImportedScripts(scriptPath, fields);
+
+ // Verify that eslint config lists files correctly
+ const expectedRulesFiles = filterScriptPaths(scripts);
+ const expectedRulesFilesSet = new Set(expectedRulesFiles);
+ const eslintConfig = loadEslint();
+ const rules = /** @type {import('core').SafeAny[]} */ (eslintConfig.overrides).find((item) => (
+ typeof item.env === 'object' &&
+ item.env !== null &&
+ item.env.worker === true
+ ));
+ assert.ok(typeof rules !== 'undefined');
+ assert.ok(Array.isArray(rules.files));
+ assert.deepStrictEqual(/** @type {import('core').SafeAny[]} */ (rules.files).filter((v) => expectedRulesFilesSet.has(v)), expectedRulesFiles);
+}
+
+
+/** */
+function main() {
+ try {
+ testServiceWorker();
+ testWorkers();
+ } catch (e) {
+ console.error(e);
+ process.exit(-1);
+ return;
+ }
+ process.exit(0);
+}
+
+
+if (require.main === module) { main(); }