summaryrefslogtreecommitdiff
path: root/ext/mixed
diff options
context:
space:
mode:
authorAlex Yatskov <alex@foosoft.net>2020-02-24 21:31:14 -0800
committerAlex Yatskov <alex@foosoft.net>2020-02-24 21:31:14 -0800
commitd32f4def0eeed1599857bc04c973337a2a13dd8b (patch)
tree61149656f361dd2d9998d67d68249dc184b73fbb /ext/mixed
parent0c5b9b1fa1599cbf769d96cdebc226310f9dd8bc (diff)
parent706c3edcffb0078d71fd5b58775f16cf5fc1205b (diff)
Merge branch 'master' into testing
Diffstat (limited to 'ext/mixed')
-rw-r--r--ext/mixed/css/display-dark.css1
-rw-r--r--ext/mixed/css/display-default.css1
-rw-r--r--ext/mixed/css/display.css60
-rw-r--r--ext/mixed/display-templates.html11
-rw-r--r--ext/mixed/js/api.js18
-rw-r--r--ext/mixed/js/audio.js28
-rw-r--r--ext/mixed/js/core.js59
-rw-r--r--ext/mixed/js/display-generator.js128
-rw-r--r--ext/mixed/js/display.js144
-rw-r--r--ext/mixed/js/japanese.js454
-rw-r--r--ext/mixed/js/template-handler.js47
-rw-r--r--ext/mixed/js/text-scanner.js66
12 files changed, 327 insertions, 690 deletions
diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css
index 088fc741..c9cd9f90 100644
--- a/ext/mixed/css/display-dark.css
+++ b/ext/mixed/css/display-dark.css
@@ -38,6 +38,7 @@ body { background-color: #1e1e1e; color: #d4d4d4; }
.tag[data-category=dictionary] { background-color: #9057ad; }
.tag[data-category=frequency] { background-color: #489148; }
.tag[data-category=partOfSpeech] { background-color: #565656; }
+.tag[data-category=search] { background-color: #69696e; }
.term-reasons { color: #888888; }
diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css
index 69141c9d..6eee43c4 100644
--- a/ext/mixed/css/display-default.css
+++ b/ext/mixed/css/display-default.css
@@ -38,6 +38,7 @@ body { background-color: #ffffff; color: #333333; }
.tag[data-category=dictionary] { background-color: #aa66cc; }
.tag[data-category=frequency] { background-color: #5cb85c; }
.tag[data-category=partOfSpeech] { background-color: #565656; }
+.tag[data-category=search] { background-color: #8a8a91; }
.term-reasons { color: #777777; }
diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css
index add2583e..6a5383bc 100644
--- a/ext/mixed/css/display.css
+++ b/ext/mixed/css/display.css
@@ -127,15 +127,19 @@ html:root[data-yomichan-page=float] .navigation-header:not([hidden])~.navigation
user-select: none;
}
-#query-parser {
+#query-parser-content {
margin-top: 0.5em;
font-size: 2em;
}
-#query-parser[data-term-spacing=true] .query-parser-term {
+#query-parser-content[data-term-spacing=true] .query-parser-term {
margin-right: 0.2em;
}
+html:root[data-yomichan-page=search][data-search-mode=popup] .search-input {
+ display: none;
+}
+
/*
* Entries
@@ -208,19 +212,27 @@ button.action-button {
}
.tag {
- display: inline;
+ display: inline-block;
padding: 0.2em 0.6em 0.3em;
font-size: 75%;
font-weight: 700;
- line-height: 1;
+ line-height: 1.25;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.25em;
}
-.tag-list>.tag+.tag {
- margin-left: 0.375em;
+.tag-inner {
+ display: block;
+}
+
+.tag-list>.tag:not(:last-child) {
+ margin-right: 0.375em;
+}
+
+html:root:not([data-enable-search-tags=true]) .tag[data-category=search] {
+ display: none;
}
.entry-header2,
@@ -237,7 +249,7 @@ button.action-button {
border-top-style: solid;
}
-.entry[data-type=term][data-expression-multi=true]:not([data-expression-count="1"]) .actions>.action-play-audio {
+.entry[data-type=term][data-expression-multi=true] .actions>.action-play-audio {
display: none;
}
@@ -245,8 +257,9 @@ button.action-button {
display: inline-block;
}
-.term-reasons>.term-reason+.term-reason:before {
+.term-reasons>.term-reason+.term-reason-separator+.term-reason:before {
content: " \00AB "; /* The two spaces is not a typo */
+ white-space: pre-wrap;
display: inline;
}
@@ -284,13 +297,13 @@ button.action-button {
content: "\3001";
}
-.term-expression-list>.term-expression:last-of-type:not(:first-of-type):after {
+.term-expression-list[data-multi=true]>.term-expression:last-of-type:after {
font-size: 2em;
content: "\3000";
visibility: hidden;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details {
+.term-expression-list[data-multi=true] .term-expression-details {
display: inline-block;
position: relative;
width: 0;
@@ -298,21 +311,21 @@ button.action-button {
visibility: hidden;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression:hover .term-expression-details {
+.term-expression-list[data-multi=true] .term-expression:hover .term-expression-details {
visibility: visible;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details>.action-play-audio {
+.term-expression-list[data-multi=true] .term-expression-details>.action-play-audio {
position: absolute;
left: 0;
bottom: 0.5em;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details>.action-play-audio {
+.term-expression-list[data-multi=true] .term-expression-details>.action-play-audio {
display: block;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details>.tags {
+.term-expression-list[data-multi=true] .term-expression-details>.tags {
display: block;
position: absolute;
left: 0;
@@ -320,7 +333,7 @@ button.action-button {
white-space: nowrap;
}
-.term-expression-list[data-multi=true]:not([data-count="1"]) .term-expression-details>.frequencies {
+.term-expression-list[data-multi=true] .term-expression-details>.frequencies {
display: block;
position: absolute;
left: 0;
@@ -385,7 +398,7 @@ button.action-button {
:root[data-compact-glossaries=true] .term-definition-tag-list,
:root[data-compact-glossaries=true] .term-definition-only-list:not([data-count="0"]) {
- display: inline-block;
+ display: inline;
}
:root[data-compact-glossaries=true] .term-glossary-list {
@@ -399,9 +412,24 @@ button.action-button {
}
:root[data-compact-glossaries=true] .term-glossary-list>li:not(:first-child):before {
+ white-space: pre-wrap;
content: " | ";
+ display: inline;
}
+.term-glossary-separator,
+.term-reason-separator {
+ display: inline;
+ font-size: 0;
+ opacity: 0;
+ white-space: pre-wrap;
+}
+
+.term-special-tags>.frequencies {
+ display: inline;
+}
+
+
/*
* Kanji
*/
diff --git a/ext/mixed/display-templates.html b/ext/mixed/display-templates.html
index 62f3c69c..7ae51a62 100644
--- a/ext/mixed/display-templates.html
+++ b/ext/mixed/display-templates.html
@@ -15,7 +15,7 @@
</div>
<div class="term-reasons"></div>
</div>
- <div class="frequencies"></div>
+ <div class="term-special-tags"><div class="frequencies tag-list"></div></div>
</div>
<div class="term-definition-container"><ol class="term-definition-list"></ol></div>
<pre class="debug-info"></pre>
@@ -31,8 +31,8 @@
<ul class="term-glossary-list"></ul>
</li></template>
<template id="term-definition-only-template"><span class="term-definition-only"></span></template>
-<template id="term-glossary-item-template"><li class="term-glossary-item"><span class="term-glossary"></span></li></template>
-<template id="term-reason-template"><span class="term-reason"></span></template>
+<template id="term-glossary-item-template"><li class="term-glossary-item"><span class="term-glossary-separator"> </span><span class="term-glossary"></span></li></template>
+<template id="term-reason-template"><span class="term-reason"></span><span class="term-reason-separator"> </span></template>
<template id="kanji-entry-template"><div class="entry" data-type="kanji">
<div class="entry-header1">
@@ -75,7 +75,8 @@
<template id="kanji-glossary-item-template"><li class="kanji-glossary-item"><span class="kanji-glossary"></span></li></template>
<template id="kanji-reading-template"><dd class="kanji-reading"></dd></template>
-<template id="tag-template"><span class="tag"></span></template>
-<template id="tag-frequency-template"><span class="tag" data-category="frequency"><span class="term-frequency-dictionary-name"></span><span class="term-frequency-separator"></span><span class="term-frequency-value"></span></template>
+<template id="tag-template"><span class="tag"><span class="tag-inner"></span></span></template>
+<template id="tag-frequency-template"><span class="tag" data-category="frequency"><span class="tag-inner"><span class="term-frequency-dictionary-name"></span><span class="term-frequency-separator"></span><span class="term-frequency-value"></span></span></template>
+<template id="tag-search-template"><span class="tag" data-category="search"></span></template>
</body></html>
diff --git a/ext/mixed/js/api.js b/ext/mixed/js/api.js
index 5ec93b01..7ea68d59 100644
--- a/ext/mixed/js/api.js
+++ b/ext/mixed/js/api.js
@@ -58,15 +58,15 @@ function apiDefinitionAdd(definition, mode, context, optionsContext) {
}
function apiDefinitionsAddable(definitions, modes, optionsContext) {
- return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext}).catch(() => null);
+ return _apiInvoke('definitionsAddable', {definitions, modes, optionsContext});
}
function apiNoteView(noteId) {
return _apiInvoke('noteView', {noteId});
}
-function apiTemplateRender(template, data, dynamic) {
- return _apiInvoke('templateRender', {data, template, dynamic});
+function apiTemplateRender(template, data) {
+ return _apiInvoke('templateRender', {data, template});
}
function apiAudioGetUrl(definition, source, optionsContext) {
@@ -89,8 +89,8 @@ function apiFrameInformationGet() {
return _apiInvoke('frameInformationGet');
}
-function apiInjectStylesheet(css) {
- return _apiInvoke('injectStylesheet', {css});
+function apiInjectStylesheet(type, value) {
+ return _apiInvoke('injectStylesheet', {type, value});
}
function apiGetEnvironmentInfo() {
@@ -105,10 +105,18 @@ function apiGetDisplayTemplatesHtml() {
return _apiInvoke('getDisplayTemplatesHtml');
}
+function apiGetQueryParserTemplatesHtml() {
+ return _apiInvoke('getQueryParserTemplatesHtml');
+}
+
function apiGetZoom() {
return _apiInvoke('getZoom');
}
+function apiGetMessageToken() {
+ return _apiInvoke('getMessageToken');
+}
+
function _apiInvoke(action, params={}) {
const data = {action, params};
return new Promise((resolve, reject) => {
diff --git a/ext/mixed/js/audio.js b/ext/mixed/js/audio.js
index b0c5fa82..b5a025be 100644
--- a/ext/mixed/js/audio.js
+++ b/ext/mixed/js/audio.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global apiAudioGetUrl*/
class TextToSpeechAudio {
constructor(text, voice) {
@@ -53,7 +54,6 @@ class TextToSpeechAudio {
speechSynthesis.cancel();
speechSynthesis.speak(this._utterance);
-
} catch (e) {
// NOP
}
@@ -71,21 +71,16 @@ class TextToSpeechAudio {
const m = /^tts:[^#?]*\?([^#]*)/.exec(ttsUri);
if (m === null) { return null; }
- const searchParameters = {};
- for (const group of m[1].split('&')) {
- const sep = group.indexOf('=');
- if (sep < 0) { continue; }
- searchParameters[decodeURIComponent(group.substring(0, sep))] = decodeURIComponent(group.substring(sep + 1));
- }
-
- if (!searchParameters.text) { return null; }
+ const searchParameters = new URLSearchParams(m[1]);
+ const text = searchParameters.get('text');
+ let voice = searchParameters.get('voice');
+ if (text === null || voice === null) { return null; }
- const voice = audioGetTextToSpeechVoice(searchParameters.voice);
+ voice = audioGetTextToSpeechVoice(voice);
if (voice === null) { return null; }
- return new TextToSpeechAudio(searchParameters.text, voice);
+ return new TextToSpeechAudio(text, voice);
}
-
}
function audioGetFromUrl(url, willDownload) {
@@ -113,8 +108,11 @@ function audioGetFromUrl(url, willDownload) {
async function audioGetFromSources(expression, sources, optionsContext, willDownload, cache=null) {
const key = `${expression.expression}:${expression.reading}`;
- if (cache !== null && hasOwn(cache, expression)) {
- return cache[key];
+ if (cache !== null) {
+ const cacheValue = cache.get(expression);
+ if (typeof cacheValue !== 'undefined') {
+ return cacheValue;
+ }
}
for (let i = 0, ii = sources.length; i < ii; ++i) {
@@ -132,7 +130,7 @@ async function audioGetFromSources(expression, sources, optionsContext, willDown
}
const result = {audio, url, source};
if (cache !== null) {
- cache[key] = result;
+ cache.set(key, result);
}
return result;
} catch (e) {
diff --git a/ext/mixed/js/core.js b/ext/mixed/js/core.js
index 0142d594..83813796 100644
--- a/ext/mixed/js/core.js
+++ b/ext/mixed/js/core.js
@@ -113,11 +113,7 @@ function toIterable(value) {
if (value !== null && typeof value === 'object') {
const length = value.length;
if (typeof length === 'number' && Number.isFinite(length)) {
- const array = [];
- for (let i = 0; i < length; ++i) {
- array.push(value[i]);
- }
- return array;
+ return Array.from(value);
}
}
@@ -128,6 +124,14 @@ function stringReverse(string) {
return string.split('').reverse().join('').replace(/([\uDC00-\uDFFF])([\uD800-\uDBFF])/g, '$2$1');
}
+function parseUrl(url) {
+ const parsedUrl = new URL(url);
+ const baseUrl = `${parsedUrl.origin}${parsedUrl.pathname}`;
+ const queryParams = Array.from(parsedUrl.searchParams.entries())
+ .reduce((a, [k, v]) => Object.assign({}, a, {[k]: v}), {});
+ return {baseUrl, queryParams};
+}
+
/*
* Async utilities
@@ -156,9 +160,9 @@ function promiseTimeout(delay, resolveValue) {
const resolve = (value) => complete(promiseResolve, value);
const reject = (value) => complete(promiseReject, value);
- const promise = new Promise((resolve, reject) => {
- promiseResolve = resolve;
- promiseReject = reject;
+ const promise = new Promise((resolve2, reject2) => {
+ promiseResolve = resolve2;
+ promiseReject = reject2;
});
timer = window.setTimeout(() => {
timer = null;
@@ -232,6 +236,29 @@ class EventDispatcher {
}
}
+class EventListenerCollection {
+ constructor() {
+ this._eventListeners = [];
+ }
+
+ get size() {
+ return this._eventListeners.length;
+ }
+
+ addEventListener(node, type, listener, options) {
+ node.addEventListener(type, listener, options);
+ this._eventListeners.push([node, type, listener, options]);
+ }
+
+ removeAllEventListeners() {
+ if (this._eventListeners.length === 0) { return; }
+ for (const [node, type, listener, options] of this._eventListeners) {
+ node.removeEventListener(type, listener, options);
+ }
+ this._eventListeners = [];
+ }
+}
+
/*
* Default message handlers
@@ -244,7 +271,7 @@ const yomichan = (() => {
this._messageHandlers = new Map([
['getUrl', this._onMessageGetUrl.bind(this)],
- ['optionsUpdate', this._onMessageOptionsUpdate.bind(this)],
+ ['optionsUpdated', this._onMessageOptionsUpdated.bind(this)],
['zoomChanged', this._onMessageZoomChanged.bind(this)]
]);
@@ -253,6 +280,16 @@ const yomichan = (() => {
// Public
+ generateId(length) {
+ const array = new Uint8Array(length);
+ window.crypto.getRandomValues(array);
+ let id = '';
+ for (const value of array) {
+ id += value.toString(16).padStart(2, '0');
+ }
+ return id;
+ }
+
triggerOrphaned(error) {
this.trigger('orphaned', {error});
}
@@ -272,8 +309,8 @@ const yomichan = (() => {
return {url: window.location.href};
}
- _onMessageOptionsUpdate({source}) {
- this.trigger('optionsUpdate', {source});
+ _onMessageOptionsUpdated({source}) {
+ this.trigger('optionsUpdated', {source});
}
_onMessageZoomChanged({oldZoomFactor, newZoomFactor}) {
diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js
index e1710488..d7e77cc0 100644
--- a/ext/mixed/js/display-generator.js
+++ b/ext/mixed/js/display-generator.js
@@ -16,46 +16,20 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
+/*global apiGetDisplayTemplatesHtml, TemplateHandler*/
class DisplayGenerator {
constructor() {
- this._isInitialized = false;
- this._initializationPromise = null;
-
- this._termEntryTemplate = null;
- this._termExpressionTemplate = null;
- this._termDefinitionItemTemplate = null;
- this._termDefinitionOnlyTemplate = null;
- this._termGlossaryItemTemplate = null;
- this._termReasonTemplate = null;
-
- this._kanjiEntryTemplate = null;
- this._kanjiInfoTableTemplate = null;
- this._kanjiInfoTableItemTemplate = null;
- this._kanjiInfoTableEmptyTemplate = null;
- this._kanjiGlossaryItemTemplate = null;
- this._kanjiReadingTemplate = null;
-
- this._tagTemplate = null;
- this._tagFrequencyTemplate = null;
+ this._templateHandler = null;
}
- isInitialized() {
- return this._isInitialized;
- }
-
- initialize() {
- if (this._isInitialized) {
- return Promise.resolve();
- }
- if (this._initializationPromise === null) {
- this._initializationPromise = this._initializeInternal();
- }
- return this._initializationPromise;
+ async prepare() {
+ const html = await apiGetDisplayTemplatesHtml();
+ this._templateHandler = new TemplateHandler(html);
}
createTermEntry(details) {
- const node = DisplayGenerator._instantiateTemplate(this._termEntryTemplate);
+ const node = this._templateHandler.instantiate('term-entry');
const expressionsContainer = node.querySelector('.term-expression-list');
const reasonsContainer = node.querySelector('.term-reasons');
@@ -71,7 +45,11 @@ class DisplayGenerator {
node.dataset.expressionCount = `${expressionMulti ? details.expressions.length : 1}`;
node.dataset.definitionCount = `${definitionMulti ? details.definitions.length : 1}`;
- DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), details.expressions, [details]);
+ const termTags = details.termTags;
+ let expressions = details.expressions;
+ expressions = Array.isArray(expressions) ? expressions.map((e) => [e, termTags]) : null;
+
+ DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), expressions, [[details, termTags]]);
DisplayGenerator._appendMultiple(reasonsContainer, this.createTermReason.bind(this), details.reasons);
DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies);
DisplayGenerator._appendMultiple(definitionsContainer, this.createTermDefinitionItem.bind(this), details.definitions, [details]);
@@ -83,8 +61,8 @@ class DisplayGenerator {
return node;
}
- createTermExpression(details) {
- const node = DisplayGenerator._instantiateTemplate(this._termExpressionTemplate);
+ createTermExpression([details, termTags]) {
+ const node = this._templateHandler.instantiate('term-expression');
const expressionContainer = node.querySelector('.term-expression-text');
const tagContainer = node.querySelector('.tags');
@@ -103,21 +81,30 @@ class DisplayGenerator {
DisplayGenerator._appendFurigana(expressionContainer, furiganaSegments, this._appendKanjiLinks.bind(this));
}
- DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), details.termTags);
+ if (!Array.isArray(termTags)) {
+ // Fallback
+ termTags = details.termTags;
+ }
+ const searchQueries = [details.expression, details.reading]
+ .filter((x) => !!x)
+ .map((x) => ({query: x}));
+ DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), termTags);
+ DisplayGenerator._appendMultiple(tagContainer, this.createSearchTag.bind(this), searchQueries);
DisplayGenerator._appendMultiple(frequencyContainer, this.createFrequencyTag.bind(this), details.frequencies);
return node;
}
createTermReason(reason) {
- const node = DisplayGenerator._instantiateTemplate(this._termReasonTemplate);
+ const fragment = this._templateHandler.instantiateFragment('term-reason');
+ const node = fragment.querySelector('.term-reason');
node.textContent = reason;
node.dataset.reason = reason;
- return node;
+ return fragment;
}
createTermDefinitionItem(details) {
- const node = DisplayGenerator._instantiateTemplate(this._termDefinitionItemTemplate);
+ const node = this._templateHandler.instantiate('term-definition-item');
const tagListContainer = node.querySelector('.term-definition-tag-list');
const onlyListContainer = node.querySelector('.term-definition-only-list');
@@ -133,7 +120,7 @@ class DisplayGenerator {
}
createTermGlossaryItem(glossary) {
- const node = DisplayGenerator._instantiateTemplate(this._termGlossaryItemTemplate);
+ const node = this._templateHandler.instantiate('term-glossary-item');
const container = node.querySelector('.term-glossary');
if (container !== null) {
DisplayGenerator._appendMultilineText(container, glossary);
@@ -142,7 +129,7 @@ class DisplayGenerator {
}
createTermOnly(only) {
- const node = DisplayGenerator._instantiateTemplate(this._termDefinitionOnlyTemplate);
+ const node = this._templateHandler.instantiate('term-definition-only');
node.dataset.only = only;
node.textContent = only;
return node;
@@ -157,7 +144,7 @@ class DisplayGenerator {
}
createKanjiEntry(details) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiEntryTemplate);
+ const node = this._templateHandler.instantiate('kanji-entry');
const glyphContainer = node.querySelector('.kanji-glyph');
const frequenciesContainer = node.querySelector('.frequencies');
@@ -202,7 +189,7 @@ class DisplayGenerator {
}
createKanjiGlossaryItem(glossary) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiGlossaryItemTemplate);
+ const node = this._templateHandler.instantiate('kanji-glossary-item');
const container = node.querySelector('.kanji-glossary');
if (container !== null) {
DisplayGenerator._appendMultilineText(container, glossary);
@@ -211,13 +198,13 @@ class DisplayGenerator {
}
createKanjiReading(reading) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiReadingTemplate);
+ const node = this._templateHandler.instantiate('kanji-reading');
node.textContent = reading;
return node;
}
createKanjiInfoTable(details) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiInfoTableTemplate);
+ const node = this._templateHandler.instantiate('kanji-info-table');
const container = node.querySelector('.kanji-info-table-body');
@@ -233,7 +220,7 @@ class DisplayGenerator {
}
createKanjiInfoTableItem(details) {
- const node = DisplayGenerator._instantiateTemplate(this._kanjiInfoTableItemTemplate);
+ const node = this._templateHandler.instantiate('kanji-info-table-item');
const nameNode = node.querySelector('.kanji-info-table-item-header');
const valueNode = node.querySelector('.kanji-info-table-item-value');
if (nameNode !== null) {
@@ -246,21 +233,33 @@ class DisplayGenerator {
}
createKanjiInfoTableItemEmpty() {
- return DisplayGenerator._instantiateTemplate(this._kanjiInfoTableEmptyTemplate);
+ return this._templateHandler.instantiate('kanji-info-table-empty');
}
createTag(details) {
- const node = DisplayGenerator._instantiateTemplate(this._tagTemplate);
+ const node = this._templateHandler.instantiate('tag');
+
+ const inner = node.querySelector('.tag-inner');
node.title = details.notes;
- node.textContent = details.name;
+ inner.textContent = details.name;
node.dataset.category = details.category;
return node;
}
+ createSearchTag(details) {
+ const node = this._templateHandler.instantiate('tag-search');
+
+ node.textContent = details.query;
+
+ node.dataset.query = details.query;
+
+ return node;
+ }
+
createFrequencyTag(details) {
- const node = DisplayGenerator._instantiateTemplate(this._tagFrequencyTemplate);
+ const node = this._templateHandler.instantiate('tag-frequency');
let n = node.querySelector('.term-frequency-dictionary-name');
if (n !== null) {
@@ -278,31 +277,6 @@ class DisplayGenerator {
return node;
}
- async _initializeInternal() {
- const html = await apiGetDisplayTemplatesHtml();
- const doc = new DOMParser().parseFromString(html, 'text/html');
- this._setTemplates(doc);
- }
-
- _setTemplates(doc) {
- this._termEntryTemplate = doc.querySelector('#term-entry-template');
- this._termExpressionTemplate = doc.querySelector('#term-expression-template');
- this._termDefinitionItemTemplate = doc.querySelector('#term-definition-item-template');
- this._termDefinitionOnlyTemplate = doc.querySelector('#term-definition-only-template');
- this._termGlossaryItemTemplate = doc.querySelector('#term-glossary-item-template');
- this._termReasonTemplate = doc.querySelector('#term-reason-template');
-
- this._kanjiEntryTemplate = doc.querySelector('#kanji-entry-template');
- this._kanjiInfoTableTemplate = doc.querySelector('#kanji-info-table-template');
- this._kanjiInfoTableItemTemplate = doc.querySelector('#kanji-info-table-item-template');
- this._kanjiInfoTableEmptyTemplate = doc.querySelector('#kanji-info-table-empty-template');
- this._kanjiGlossaryItemTemplate = doc.querySelector('#kanji-glossary-item-template');
- this._kanjiReadingTemplate = doc.querySelector('#kanji-reading-template');
-
- this._tagTemplate = doc.querySelector('#tag-template');
- this._tagFrequencyTemplate = doc.querySelector('#tag-frequency-template');
- }
-
_appendKanjiLinks(container, text) {
let part = '';
for (const c of text) {
@@ -372,8 +346,4 @@ class DisplayGenerator {
container.appendChild(document.createTextNode(parts[i]));
}
}
-
- static _instantiateTemplate(template) {
- return document.importNode(template.content.firstChild, true);
- }
}
diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js
index c4be02f2..5d3076ee 100644
--- a/ext/mixed/js/display.js
+++ b/ext/mixed/js/display.js
@@ -16,6 +16,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global docRangeFromPoint, docSentenceExtract
+apiKanjiFind, apiTermsFind, apiNoteView, apiOptionsGet, apiDefinitionsAddable, apiDefinitionAdd
+apiScreenshotGet, apiForward
+audioPrepareTextToSpeech, audioGetFromSources
+DisplayGenerator, WindowScroll, DisplayContext, DOM*/
class Display {
constructor(spinner, container) {
@@ -27,11 +32,11 @@ class Display {
this.index = 0;
this.audioPlaying = null;
this.audioFallback = null;
- this.audioCache = {};
+ this.audioCache = new Map();
this.styleNode = null;
- this.eventListeners = [];
- this.persistentEventListeners = [];
+ this.eventListeners = new EventListenerCollection();
+ this.persistentEventListeners = new EventListenerCollection();
this.interactive = false;
this.eventListenersActive = false;
this.clickScanPrevent = false;
@@ -43,6 +48,13 @@ class Display {
this.setInteractive(true);
}
+ async prepare(options=null) {
+ const displayGeneratorPromise = this.displayGenerator.prepare();
+ const updateOptionsPromise = this.updateOptions(options);
+ await Promise.all([displayGeneratorPromise, updateOptionsPromise]);
+ yomichan.on('optionsUpdated', () => this.updateOptions(null));
+ }
+
onError(_error) {
throw new Error('Override me');
}
@@ -174,15 +186,24 @@ class Display {
e.preventDefault();
const link = e.currentTarget;
const entry = link.closest('.entry');
- const definitionIndex = this.entryIndexFind(entry);
+ const index = this.entryIndexFind(entry);
+ if (index < 0 || index >= this.definitions.length) { return; }
+
const expressionIndex = Display.indexOf(entry.querySelectorAll('.term-expression .action-play-audio'), link);
- this.audioPlay(this.definitions[definitionIndex], expressionIndex, definitionIndex);
+ this.audioPlay(
+ this.definitions[index],
+ // expressionIndex is used in audioPlay to detect result output mode
+ Math.max(expressionIndex, this.options.general.resultOutputMode === 'merge' ? 0 : -1),
+ index
+ );
}
onNoteAdd(e) {
e.preventDefault();
const link = e.currentTarget;
const index = this.entryIndexFind(link);
+ if (index < 0 || index >= this.definitions.length) { return; }
+
this.noteAdd(this.definitions[index], link.dataset.mode);
}
@@ -216,13 +237,16 @@ class Display {
}
onHistoryWheel(e) {
+ if (e.altKey) { return; }
const delta = -e.deltaX || e.deltaY;
if (delta > 0) {
this.sourceTermView();
e.preventDefault();
+ e.stopPropagation();
} else if (delta < 0) {
this.nextTermView();
e.preventDefault();
+ e.stopPropagation();
}
}
@@ -230,15 +254,6 @@ class Display {
throw new Error('Override me');
}
- isInitialized() {
- return this.options !== null;
- }
-
- async initialize(options=null) {
- await this.updateOptions(options);
- yomichan.on('optionsUpdate', () => this.updateOptions(null));
- }
-
async updateOptions(options) {
this.options = options ? options : await apiOptionsGet(this.getOptionsContext());
this.updateDocumentOptions(this.options);
@@ -252,6 +267,7 @@ class Display {
data.ankiEnabled = `${options.anki.enable}`;
data.audioEnabled = `${options.audio.enable}`;
data.compactGlossaries = `${options.general.compactGlossaries}`;
+ data.enableSearchTags = `${options.scanning.enableSearchTags}`;
data.debug = `${options.general.debugInfo}`;
}
@@ -285,13 +301,24 @@ class Display {
this.interactive = interactive;
if (interactive) {
- Display.addEventListener(this.persistentEventListeners, document, 'keydown', this.onKeyDown.bind(this), false);
- Display.addEventListener(this.persistentEventListeners, document, 'wheel', this.onWheel.bind(this), {passive: false});
- Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-previous'), 'click', this.onSourceTermView.bind(this));
- Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-next'), 'click', this.onNextTermView.bind(this));
- Display.addEventListener(this.persistentEventListeners, document.querySelector('.navigation-header'), 'wheel', this.onHistoryWheel.bind(this), {passive: false});
+ const actionPrevious = document.querySelector('.action-previous');
+ const actionNext = document.querySelector('.action-next');
+ // const navigationHeader = document.querySelector('.navigation-header');
+
+ this.persistentEventListeners.addEventListener(document, 'keydown', this.onKeyDown.bind(this), false);
+ this.persistentEventListeners.addEventListener(document, 'wheel', this.onWheel.bind(this), {passive: false});
+ if (actionPrevious !== null) {
+ this.persistentEventListeners.addEventListener(actionPrevious, 'click', this.onSourceTermView.bind(this));
+ }
+ if (actionNext !== null) {
+ this.persistentEventListeners.addEventListener(actionNext, 'click', this.onNextTermView.bind(this));
+ }
+ // temporarily disabled
+ // if (navigationHeader !== null) {
+ // this.persistentEventListeners.addEventListener(navigationHeader, 'wheel', this.onHistoryWheel.bind(this), {passive: false});
+ // }
} else {
- Display.clearEventListeners(this.persistentEventListeners);
+ this.persistentEventListeners.removeAllEventListeners();
}
this.setEventListenersActive(this.eventListenersActive);
}
@@ -302,23 +329,23 @@ class Display {
this.eventListenersActive = active;
if (active) {
- this.addEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
- this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
- this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
- this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
+ this.addMultipleEventListeners('.action-add-note', 'click', this.onNoteAdd.bind(this));
+ this.addMultipleEventListeners('.action-view-note', 'click', this.onNoteView.bind(this));
+ this.addMultipleEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this));
+ this.addMultipleEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this));
if (this.options.scanning.enablePopupSearch) {
- this.addEventListeners('.glossary-item', 'mouseup', this.onGlossaryMouseUp.bind(this));
- this.addEventListeners('.glossary-item', 'mousedown', this.onGlossaryMouseDown.bind(this));
- this.addEventListeners('.glossary-item', 'mousemove', this.onGlossaryMouseMove.bind(this));
+ this.addMultipleEventListeners('.term-glossary-item, .tag', 'mouseup', this.onGlossaryMouseUp.bind(this));
+ this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousedown', this.onGlossaryMouseDown.bind(this));
+ this.addMultipleEventListeners('.term-glossary-item, .tag', 'mousemove', this.onGlossaryMouseMove.bind(this));
}
} else {
- Display.clearEventListeners(this.eventListeners);
+ this.eventListeners.removeAllEventListeners();
}
}
- addEventListeners(selector, type, listener, options) {
+ addMultipleEventListeners(selector, type, listener, options) {
for (const node of this.container.querySelectorAll(selector)) {
- Display.addEventListener(this.eventListeners, node, type, listener, options);
+ this.eventListeners.addEventListener(node, type, listener, options);
}
}
@@ -348,7 +375,6 @@ class Display {
async setContentTerms(definitions, context, token) {
if (!context) { throw new Error('Context expected'); }
- if (!this.isInitialized()) { return; }
this.setEventListenersActive(false);
@@ -356,11 +382,6 @@ class Display {
window.focus();
}
- if (!this.displayGenerator.isInitialized()) {
- await this.displayGenerator.initialize();
- if (this.setContentToken !== token) { return; }
- }
-
this.definitions = definitions;
if (context.disableHistory) {
delete context.disableHistory;
@@ -404,7 +425,7 @@ class Display {
this.setEventListenersActive(true);
- const states = await apiDefinitionsAddable(definitions, ['term-kanji', 'term-kana'], this.getOptionsContext());
+ const states = await this.getDefinitionsAddable(definitions, ['term-kanji', 'term-kana']);
if (this.setContentToken !== token) { return; }
this.updateAdderButtons(states);
@@ -412,7 +433,6 @@ class Display {
async setContentKanji(definitions, context, token) {
if (!context) { throw new Error('Context expected'); }
- if (!this.isInitialized()) { return; }
this.setEventListenersActive(false);
@@ -420,11 +440,6 @@ class Display {
window.focus();
}
- if (!this.displayGenerator.isInitialized()) {
- await this.displayGenerator.initialize();
- if (this.setContentToken !== token) { return; }
- }
-
this.definitions = definitions;
if (context.disableHistory) {
delete context.disableHistory;
@@ -446,7 +461,7 @@ class Display {
for (let i = 0, ii = definitions.length; i < ii; ++i) {
if (i > 0) {
- await promiseTimeout(0);
+ await promiseTimeout(1);
if (this.setContentToken !== token) { return; }
}
@@ -459,7 +474,7 @@ class Display {
this.setEventListenersActive(true);
- const states = await apiDefinitionsAddable(definitions, ['kanji'], this.getOptionsContext());
+ const states = await this.getDefinitionsAddable(definitions, ['kanji']);
if (this.setContentToken !== token) { return; }
this.updateAdderButtons(states);
@@ -498,6 +513,8 @@ class Display {
}
autoPlayAudio() {
+ if (this.definitions.length === 0) { return; }
+
this.audioPlay(this.definitions[0], this.firstExpressionIndex, 0);
}
@@ -597,9 +614,12 @@ class Display {
}
noteTryAdd(mode) {
- const button = this.adderButtonFind(this.index, mode);
+ const index = this.index;
+ if (index < 0 || index >= this.definitions.length) { return; }
+
+ const button = this.adderButtonFind(index, mode);
if (button !== null && !button.classList.contains('disabled')) {
- this.noteAdd(this.definitions[this.index], mode);
+ this.noteAdd(this.definitions[index], mode);
}
}
@@ -698,7 +718,7 @@ class Display {
async getScreenshot() {
try {
await this.setPopupVisibleOverride(false);
- await Display.delay(1); // Wait for popup to be hidden.
+ await promiseTimeout(1); // Wait for popup to be hidden.
const {format, quality} = this.options.anki.screenshot;
const dataUrl = await apiScreenshotGet({format, quality});
@@ -767,8 +787,12 @@ class Display {
return entry !== null ? entry.querySelector('.action-play-audio>img') : null;
}
- static delay(time) {
- return new Promise((resolve) => setTimeout(resolve, time));
+ async getDefinitionsAddable(definitions, modes) {
+ try {
+ return await apiDefinitionsAddable(definitions, modes, this.getOptionsContext());
+ } catch (e) {
+ return [];
+ }
}
static indexOf(nodeList, node) {
@@ -780,19 +804,6 @@ class Display {
return -1;
}
- static addEventListener(eventListeners, object, type, listener, options) {
- if (object === null) { return; }
- object.addEventListener(type, listener, options);
- eventListeners.push([object, type, listener, options]);
- }
-
- static clearEventListeners(eventListeners) {
- for (const [object, type, listener, options] of eventListeners) {
- object.removeEventListener(type, listener, options);
- }
- eventListeners.length = 0;
- }
-
static getElementTop(element) {
const elementRect = element.getBoundingClientRect();
const documentRect = document.documentElement.getBoundingClientRect();
@@ -901,9 +912,12 @@ Display._onKeyDownHandlers = new Map([
['P', (self, e) => {
if (e.altKey) {
- const entry = self.getEntry(self.index);
+ const index = self.index;
+ if (index < 0 || index >= self.definitions.length) { return; }
+
+ const entry = self.getEntry(index);
if (entry !== null && entry.dataset.type === 'term') {
- self.audioPlay(self.definitions[self.index], self.firstExpressionIndex, self.index);
+ self.audioPlay(self.definitions[index], self.firstExpressionIndex, index);
}
return true;
}
diff --git a/ext/mixed/js/japanese.js b/ext/mixed/js/japanese.js
deleted file mode 100644
index 0da822d7..00000000
--- a/ext/mixed/js/japanese.js
+++ /dev/null
@@ -1,454 +0,0 @@
-/*
- * Copyright (C) 2016-2020 Alex Yatskov <alex@foosoft.net>
- * Author: Alex Yatskov <alex@foosoft.net>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
-
-
-const JP_HALFWIDTH_KATAKANA_MAPPING = new Map([
- ['ヲ', 'ヲヺ-'],
- ['ァ', 'ァ--'],
- ['ィ', 'ィ--'],
- ['ゥ', 'ゥ--'],
- ['ェ', 'ェ--'],
- ['ォ', 'ォ--'],
- ['ャ', 'ャ--'],
- ['ュ', 'ュ--'],
- ['ョ', 'ョ--'],
- ['ッ', 'ッ--'],
- ['ー', 'ー--'],
- ['ア', 'ア--'],
- ['イ', 'イ--'],
- ['ウ', 'ウヴ-'],
- ['エ', 'エ--'],
- ['オ', 'オ--'],
- ['カ', 'カガ-'],
- ['キ', 'キギ-'],
- ['ク', 'クグ-'],
- ['ケ', 'ケゲ-'],
- ['コ', 'コゴ-'],
- ['サ', 'サザ-'],
- ['シ', 'シジ-'],
- ['ス', 'スズ-'],
- ['セ', 'セゼ-'],
- ['ソ', 'ソゾ-'],
- ['タ', 'タダ-'],
- ['チ', 'チヂ-'],
- ['ツ', 'ツヅ-'],
- ['テ', 'テデ-'],
- ['ト', 'トド-'],
- ['ナ', 'ナ--'],
- ['ニ', 'ニ--'],
- ['ヌ', 'ヌ--'],
- ['ネ', 'ネ--'],
- ['ノ', 'ノ--'],
- ['ハ', 'ハバパ'],
- ['ヒ', 'ヒビピ'],
- ['フ', 'フブプ'],
- ['ヘ', 'ヘベペ'],
- ['ホ', 'ホボポ'],
- ['マ', 'マ--'],
- ['ミ', 'ミ--'],
- ['ム', 'ム--'],
- ['メ', 'メ--'],
- ['モ', 'モ--'],
- ['ヤ', 'ヤ--'],
- ['ユ', 'ユ--'],
- ['ヨ', 'ヨ--'],
- ['ラ', 'ラ--'],
- ['リ', 'リ--'],
- ['ル', 'ル--'],
- ['レ', 'レ--'],
- ['ロ', 'ロ--'],
- ['ワ', 'ワ--'],
- ['ン', 'ン--']
-]);
-
-const JP_HIRAGANA_RANGE = [0x3040, 0x309f];
-const JP_KATAKANA_RANGE = [0x30a0, 0x30ff];
-const JP_KANA_RANGES = [JP_HIRAGANA_RANGE, JP_KATAKANA_RANGE];
-
-const JP_CJK_COMMON_RANGE = [0x4e00, 0x9fff];
-const JP_CJK_RARE_RANGE = [0x3400, 0x4dbf];
-const JP_CJK_RANGES = [JP_CJK_COMMON_RANGE, JP_CJK_RARE_RANGE];
-
-const JP_ITERATION_MARK_CHAR_CODE = 0x3005;
-
-// Japanese character ranges, roughly ordered in order of expected frequency
-const JP_JAPANESE_RANGES = [
- JP_HIRAGANA_RANGE,
- JP_KATAKANA_RANGE,
-
- JP_CJK_COMMON_RANGE,
- JP_CJK_RARE_RANGE,
-
- [0xff66, 0xff9f], // Halfwidth katakana
-
- [0x30fb, 0x30fc], // Katakana punctuation
- [0xff61, 0xff65], // Kana punctuation
- [0x3000, 0x303f], // CJK punctuation
-
- [0xff10, 0xff19], // Fullwidth numbers
- [0xff21, 0xff3a], // Fullwidth upper case Latin letters
- [0xff41, 0xff5a], // Fullwidth lower case Latin letters
-
- [0xff01, 0xff0f], // Fullwidth punctuation 1
- [0xff1a, 0xff1f], // Fullwidth punctuation 2
- [0xff3b, 0xff3f], // Fullwidth punctuation 3
- [0xff5b, 0xff60], // Fullwidth punctuation 4
- [0xffe0, 0xffee], // Currency markers
-];
-
-
-// Helper functions
-
-function _jpIsCharCodeInRanges(charCode, ranges) {
- for (const [min, max] of ranges) {
- if (charCode >= min && charCode <= max) {
- return true;
- }
- }
- return false;
-}
-
-
-// Character code testing functions
-
-function jpIsCharCodeKanji(charCode) {
- return _jpIsCharCodeInRanges(charCode, JP_CJK_RANGES);
-}
-
-function jpIsCharCodeKana(charCode) {
- return _jpIsCharCodeInRanges(charCode, JP_KANA_RANGES);
-}
-
-function jpIsCharCodeJapanese(charCode) {
- return _jpIsCharCodeInRanges(charCode, JP_JAPANESE_RANGES);
-}
-
-
-// String testing functions
-
-function jpIsStringEntirelyKana(str) {
- if (str.length === 0) { return false; }
- for (let i = 0, ii = str.length; i < ii; ++i) {
- if (!jpIsCharCodeKana(str.charCodeAt(i))) {
- return false;
- }
- }
- return true;
-}
-
-function jpIsStringPartiallyJapanese(str) {
- if (str.length === 0) { return false; }
- for (let i = 0, ii = str.length; i < ii; ++i) {
- if (jpIsCharCodeJapanese(str.charCodeAt(i))) {
- return true;
- }
- }
- return false;
-}
-
-
-// Conversion functions
-
-function jpKatakanaToHiragana(text) {
- let result = '';
- for (const c of text) {
- if (wanakana.isKatakana(c)) {
- result += wanakana.toHiragana(c);
- } else {
- result += c;
- }
- }
-
- return result;
-}
-
-function jpHiraganaToKatakana(text) {
- let result = '';
- for (const c of text) {
- if (wanakana.isHiragana(c)) {
- result += wanakana.toKatakana(c);
- } else {
- result += c;
- }
- }
-
- return result;
-}
-
-function jpToRomaji(text) {
- return wanakana.toRomaji(text);
-}
-
-function jpConvertReading(expressionFragment, readingFragment, readingMode) {
- switch (readingMode) {
- case 'hiragana':
- return jpKatakanaToHiragana(readingFragment || '');
- case 'katakana':
- return jpHiraganaToKatakana(readingFragment || '');
- case 'romaji':
- if (readingFragment) {
- return jpToRomaji(readingFragment);
- } else {
- if (jpIsStringEntirelyKana(expressionFragment)) {
- return jpToRomaji(expressionFragment);
- }
- }
- return readingFragment;
- case 'none':
- return null;
- default:
- return readingFragment;
- }
-}
-
-function jpDistributeFurigana(expression, reading) {
- const fallback = [{furigana: reading, text: expression}];
- if (!reading) {
- return fallback;
- }
-
- let isAmbiguous = false;
- const segmentize = (reading, groups) => {
- if (groups.length === 0 || isAmbiguous) {
- return [];
- }
-
- const group = groups[0];
- if (group.mode === 'kana') {
- if (jpKatakanaToHiragana(reading).startsWith(jpKatakanaToHiragana(group.text))) {
- const readingLeft = reading.substring(group.text.length);
- const segs = segmentize(readingLeft, groups.splice(1));
- if (segs) {
- return [{text: group.text}].concat(segs);
- }
- }
- } else {
- let foundSegments = null;
- for (let i = reading.length; i >= group.text.length; --i) {
- const readingUsed = reading.substring(0, i);
- const readingLeft = reading.substring(i);
- const segs = segmentize(readingLeft, groups.slice(1));
- if (segs) {
- if (foundSegments !== null) {
- // more than one way to segmentize the tail, mark as ambiguous
- isAmbiguous = true;
- return null;
- }
- foundSegments = [{text: group.text, furigana: readingUsed}].concat(segs);
- }
- // there is only one way to segmentize the last non-kana group
- if (groups.length === 1) {
- break;
- }
- }
- return foundSegments;
- }
- };
-
- const groups = [];
- let modePrev = null;
- for (const c of expression) {
- const charCode = c.charCodeAt(0);
- const modeCurr = jpIsCharCodeKanji(charCode) || charCode === JP_ITERATION_MARK_CHAR_CODE ? 'kanji' : 'kana';
- if (modeCurr === modePrev) {
- groups[groups.length - 1].text += c;
- } else {
- groups.push({mode: modeCurr, text: c});
- modePrev = modeCurr;
- }
- }
-
- const segments = segmentize(reading, groups);
- if (segments && !isAmbiguous) {
- return segments;
- }
- return fallback;
-}
-
-function jpDistributeFuriganaInflected(expression, reading, source) {
- const output = [];
-
- let stemLength = 0;
- const shortest = Math.min(source.length, expression.length);
- const sourceHiragana = jpKatakanaToHiragana(source);
- const expressionHiragana = jpKatakanaToHiragana(expression);
- while (stemLength < shortest && sourceHiragana[stemLength] === expressionHiragana[stemLength]) {
- ++stemLength;
- }
- const offset = source.length - stemLength;
-
- const stemExpression = source.substring(0, source.length - offset);
- const stemReading = reading.substring(
- 0,
- offset === 0 ? reading.length : reading.length - expression.length + stemLength
- );
- for (const segment of jpDistributeFurigana(stemExpression, stemReading)) {
- output.push(segment);
- }
-
- if (stemLength !== source.length) {
- output.push({text: source.substring(stemLength)});
- }
-
- return output;
-}
-
-function jpConvertHalfWidthKanaToFullWidth(text, sourceMapping) {
- let result = '';
- const ii = text.length;
- const hasSourceMapping = Array.isArray(sourceMapping);
-
- for (let i = 0; i < ii; ++i) {
- const c = text[i];
- const mapping = JP_HALFWIDTH_KATAKANA_MAPPING.get(c);
- if (typeof mapping !== 'string') {
- result += c;
- continue;
- }
-
- let index = 0;
- switch (text.charCodeAt(i + 1)) {
- case 0xff9e: // dakuten
- index = 1;
- break;
- case 0xff9f: // handakuten
- index = 2;
- break;
- }
-
- let c2 = mapping[index];
- if (index > 0) {
- if (c2 === '-') { // invalid
- index = 0;
- c2 = mapping[0];
- } else {
- ++i;
- }
- }
-
- if (hasSourceMapping && index > 0) {
- index = result.length;
- const v = sourceMapping.splice(index + 1, 1)[0];
- sourceMapping[index] += v;
- }
- result += c2;
- }
-
- return result;
-}
-
-function jpConvertNumericTofullWidth(text) {
- let result = '';
- for (let i = 0, ii = text.length; i < ii; ++i) {
- let c = text.charCodeAt(i);
- if (c >= 0x30 && c <= 0x39) { // ['0', '9']
- c += 0xff10 - 0x30; // 0xff10 = '0' full width
- result += String.fromCharCode(c);
- } else {
- result += text[i];
- }
- }
- return result;
-}
-
-function jpConvertAlphabeticToKana(text, sourceMapping) {
- let part = '';
- let result = '';
- const ii = text.length;
-
- if (sourceMapping.length === ii) {
- sourceMapping.length = ii;
- sourceMapping.fill(1);
- }
-
- for (let i = 0; i < ii; ++i) {
- // Note: 0x61 is the character code for 'a'
- let c = text.charCodeAt(i);
- if (c >= 0x41 && c <= 0x5a) { // ['A', 'Z']
- c += (0x61 - 0x41);
- } else if (c >= 0x61 && c <= 0x7a) { // ['a', 'z']
- // NOP; c += (0x61 - 0x61);
- } else if (c >= 0xff21 && c <= 0xff3a) { // ['A', 'Z'] fullwidth
- c += (0x61 - 0xff21);
- } else if (c >= 0xff41 && c <= 0xff5a) { // ['a', 'z'] fullwidth
- c += (0x61 - 0xff41);
- } else if (c === 0x2d || c === 0xff0d) { // '-' or fullwidth dash
- c = 0x2d; // '-'
- } else {
- if (part.length > 0) {
- result += jpToHiragana(part, sourceMapping, result.length);
- part = '';
- }
- result += text[i];
- continue;
- }
- part += String.fromCharCode(c);
- }
-
- if (part.length > 0) {
- result += jpToHiragana(part, sourceMapping, result.length);
- }
- return result;
-}
-
-function jpToHiragana(text, sourceMapping, sourceMappingStart) {
- const result = wanakana.toHiragana(text);
-
- // Generate source mapping
- if (Array.isArray(sourceMapping)) {
- if (typeof sourceMappingStart !== 'number') { sourceMappingStart = 0; }
- let i = 0;
- let resultPos = 0;
- const ii = text.length;
- while (i < ii) {
- // Find smallest matching substring
- let iNext = i + 1;
- let resultPosNext = result.length;
- while (iNext < ii) {
- const t = wanakana.toHiragana(text.substring(0, iNext));
- if (t === result.substring(0, t.length)) {
- resultPosNext = t.length;
- break;
- }
- ++iNext;
- }
-
- // Merge characters
- const removals = iNext - i - 1;
- if (removals > 0) {
- let sum = 0;
- const vs = sourceMapping.splice(sourceMappingStart + 1, removals);
- for (const v of vs) { sum += v; }
- sourceMapping[sourceMappingStart] += sum;
- }
- ++sourceMappingStart;
-
- // Empty elements
- const additions = resultPosNext - resultPos - 1;
- for (let j = 0; j < additions; ++j) {
- sourceMapping.splice(sourceMappingStart, 0, 0);
- ++sourceMappingStart;
- }
-
- i = iNext;
- resultPos = resultPosNext;
- }
- }
-
- return result;
-}
diff --git a/ext/mixed/js/template-handler.js b/ext/mixed/js/template-handler.js
new file mode 100644
index 00000000..a5a62937
--- /dev/null
+++ b/ext/mixed/js/template-handler.js
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 Alex Yatskov <alex@foosoft.net>
+ * Author: Alex Yatskov <alex@foosoft.net>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+class TemplateHandler {
+ constructor(html) {
+ this._templates = new Map();
+
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ for (const template of doc.querySelectorAll('template')) {
+ this._setTemplate(template);
+ }
+ }
+
+ _setTemplate(template) {
+ const idMatch = template.id.match(/^([a-z-]+)-template$/);
+ if (!idMatch) {
+ throw new Error(`Invalid template ID: ${template.id}`);
+ }
+ this._templates.set(idMatch[1], template);
+ }
+
+ instantiate(name) {
+ const template = this._templates.get(name);
+ return document.importNode(template.content.firstChild, true);
+ }
+
+ instantiateFragment(name) {
+ const template = this._templates.get(name);
+ return document.importNode(template.content, true);
+ }
+}
diff --git a/ext/mixed/js/text-scanner.js b/ext/mixed/js/text-scanner.js
index 88f1e27a..ff0eac8b 100644
--- a/ext/mixed/js/text-scanner.js
+++ b/ext/mixed/js/text-scanner.js
@@ -16,6 +16,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+/*global docRangeFromPoint, TextSourceRange, DOM*/
class TextScanner {
constructor(node, ignoreNodes, ignoreElements, ignorePoints) {
@@ -30,7 +31,7 @@ class TextScanner {
this.options = null;
this.enabled = false;
- this.eventListeners = [];
+ this.eventListeners = new EventListenerCollection();
this.primaryTouchIdentifier = null;
this.preventNextContextMenu = false;
@@ -140,24 +141,24 @@ class TextScanner {
const textSourceCurrentPrevious = this.textSourceCurrent !== null ? this.textSourceCurrent.clone() : null;
this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchStart')
- .then(() => {
- if (
- this.textSourceCurrent === null ||
- this.textSourceCurrent.equals(textSourceCurrentPrevious)
- ) {
- return;
- }
+ .then(() => {
+ if (
+ this.textSourceCurrent === null ||
+ this.textSourceCurrent.equals(textSourceCurrentPrevious)
+ ) {
+ return;
+ }
- this.preventScroll = true;
- this.preventNextContextMenu = true;
- this.preventNextMouseDown = true;
- });
+ this.preventScroll = true;
+ this.preventNextContextMenu = true;
+ this.preventNextMouseDown = true;
+ });
}
onTouchEnd(e) {
if (
this.primaryTouchIdentifier === null ||
- TextScanner.getIndexOfTouch(e.changedTouches, this.primaryTouchIdentifier) < 0
+ TextScanner.getTouch(e.changedTouches, this.primaryTouchIdentifier) === null
) {
return;
}
@@ -180,13 +181,11 @@ class TextScanner {
return;
}
- const touches = e.changedTouches;
- const index = TextScanner.getIndexOfTouch(touches, this.primaryTouchIdentifier);
- if (index < 0) {
+ const primaryTouch = TextScanner.getTouch(e.changedTouches, this.primaryTouchIdentifier);
+ if (primaryTouch === null) {
return;
}
- const primaryTouch = touches[index];
this.searchAt(primaryTouch.clientX, primaryTouch.clientY, 'touchMove');
e.preventDefault(); // Disable scroll
@@ -228,7 +227,7 @@ class TextScanner {
}
} else {
if (this.enabled) {
- this.clearEventListeners();
+ this.eventListeners.removeAllEventListeners();
this.enabled = false;
}
this.onSearchClear(false);
@@ -236,13 +235,13 @@ class TextScanner {
}
hookEvents() {
- let eventListeners = this.getMouseEventListeners();
+ let eventListenerInfos = this.getMouseEventListeners();
if (this.options.scanning.touchInputEnabled) {
- eventListeners = eventListeners.concat(this.getTouchEventListeners());
+ eventListenerInfos = eventListenerInfos.concat(this.getTouchEventListeners());
}
- for (const [node, type, listener, options] of eventListeners) {
- this.addEventListener(node, type, listener, options);
+ for (const [node, type, listener, options] of eventListenerInfos) {
+ this.eventListeners.addEventListener(node, type, listener, options);
}
}
@@ -267,18 +266,6 @@ class TextScanner {
];
}
- addEventListener(node, type, listener, options) {
- node.addEventListener(type, listener, options);
- this.eventListeners.push([node, type, listener, options]);
- }
-
- clearEventListeners() {
- for (const [node, type, listener, options] of this.eventListeners) {
- node.removeEventListener(type, listener, options);
- }
- this.eventListeners = [];
- }
-
setOptions(options) {
this.options = options;
this.setEnabled(this.options.general.enable);
@@ -367,13 +354,12 @@ class TextScanner {
}
}
- static getIndexOfTouch(touchList, identifier) {
- for (const i in touchList) {
- const t = touchList[i];
- if (t.identifier === identifier) {
- return i;
+ static getTouch(touchList, identifier) {
+ for (const touch of touchList) {
+ if (touch.identifier === identifier) {
+ return touch;
}
}
- return -1;
+ return null;
}
}