summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/bg/js/backend.js23
-rw-r--r--ext/bg/js/mecab.js232
2 files changed, 167 insertions, 88 deletions
diff --git a/ext/bg/js/backend.js b/ext/bg/js/backend.js
index 55905fab..67b17cc9 100644
--- a/ext/bg/js/backend.js
+++ b/ext/bg/js/backend.js
@@ -877,11 +877,7 @@ class Backend {
this._anki.server = options.anki.server;
this._anki.enabled = options.anki.enable;
- if (options.parsing.enableMecabParser) {
- this._mecab.startListener();
- } else {
- this._mecab.stopListener();
- }
+ this._mecab.setEnabled(options.parsing.enableMecabParser);
if (options.clipboard.enableBackgroundMonitor) {
this._clipboardMonitor.start();
@@ -988,12 +984,19 @@ class Backend {
async _textParseMecab(text, options) {
const jp = this._japaneseUtil;
const {parsing: {readingMode}} = options;
+
+ let parseTextResults;
+ try {
+ parseTextResults = await this._mecab.parseText(text);
+ } catch (e) {
+ return [];
+ }
+
const results = [];
- const rawResults = await this._mecab.parseText(text);
- for (const [mecabName, parsedLines] of Object.entries(rawResults)) {
+ for (const {name, lines} of parseTextResults) {
const result = [];
- for (const parsedLine of parsedLines) {
- for (const {expression, reading, source} of parsedLine) {
+ for (const line of lines) {
+ for (const {expression, reading, source} of line) {
const term = [];
for (const {text: text2, furigana} of jp.distributeFuriganaInflected(
expression.length > 0 ? expression : source,
@@ -1007,7 +1010,7 @@ class Backend {
}
result.push([{text: '\n', reading: ''}]);
}
- results.push([mecabName, result]);
+ results.push([name, result]);
}
return results;
}
diff --git a/ext/bg/js/mecab.js b/ext/bg/js/mecab.js
index 238f7a7a..ef5d6821 100644
--- a/ext/bg/js/mecab.js
+++ b/ext/bg/js/mecab.js
@@ -15,107 +15,183 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
-
+/**
+ * This class is used to connect Yomichan to a native component that is
+ * used to parse text into individual terms.
+ */
class Mecab {
+ /**
+ * Creates a new instance of the class.
+ */
constructor() {
- this.port = null;
- this.listeners = new Map();
- this.sequence = 0;
+ this._port = null;
+ this._sequence = 0;
+ this._invocations = new Map();
+ this._eventListeners = new EventListenerCollection();
+ this._timeout = 5000;
+ this._version = 1;
+ this._remoteVersion = null;
+ this._enabled = false;
+ this._setupPortPromise = null;
}
- onError(error) {
- yomichan.logError(error);
+ /**
+ * Returns whether or not the component is enabled.
+ */
+ isEnabled() {
+ return this._enabled;
}
- async checkVersion() {
- try {
- const {version} = await this.invoke('get_version', {});
- if (version !== Mecab.version) {
- this.stopListener();
- throw new Error(`Unsupported MeCab native messenger version ${version}. Yomichan supports version ${Mecab.version}.`);
- }
- } catch (error) {
- this.onError(error);
+ /**
+ * Changes whether or not the component connection is enabled.
+ * @param enabled A boolean indicating whether or not the component should be enabled.
+ */
+ setEnabled(enabled) {
+ this._enabled = !!enabled;
+ if (!this._enabled && this._port !== null) {
+ this._clearPort();
}
}
- async parseText(text) {
- const rawResults = await this.invoke('parse_text', {text});
- // {
- // 'mecab-name': [
- // // line1
- // [
- // {str expression: 'expression', str reading: 'reading', str source: 'source'},
- // {str expression: 'expression2', str reading: 'reading2', str source: 'source2'}
- // ],
- // line2,
- // ...
- // ],
- // 'mecab-name2': [...]
- // }
- const results = {};
- for (const [mecabName, parsedLines] of Object.entries(rawResults)) {
- const result = [];
- for (const parsedLine of parsedLines) {
- const line = [];
- for (const {expression, reading, source} of parsedLine) {
- line.push({
- expression: expression || '',
- reading: reading || '',
- source: source || ''
- });
- }
- result.push(line);
- }
- results[mecabName] = result;
+ /**
+ * Gets the version of the MeCab component.
+ * @returns The version of the MeCab component, or `null` if the component was not found.
+ */
+ async getVersion() {
+ try {
+ await this._setupPort();
+ } catch (e) {
+ // NOP
}
- return results;
+ return this._remoteVersion;
}
- startListener() {
- if (this.port !== null) { return; }
- this.port = chrome.runtime.connectNative('yomichan_mecab');
- this.port.onMessage.addListener(this.onNativeMessage.bind(this));
- this.checkVersion();
+ /**
+ * Parses a string of Japanese text into arrays of lines and terms.
+ *
+ * Return value format:
+ * ```js
+ * [
+ * {
+ * name: (string),
+ * lines: [
+ * {expression: (string), reading: (string), source: (string)},
+ * ...
+ * ]
+ * },
+ * ...
+ * ]
+ * ```
+ * @param text The string to parse.
+ * @returns A collection of parsing results of the text.
+ */
+ async parseText(text) {
+ await this._setupPort();
+ const rawResults = await this._invoke('parse_text', {text});
+ return this._convertParseTextResults(rawResults);
}
- stopListener() {
- if (this.port === null) { return; }
- this.port.disconnect();
- this.port = null;
- this.listeners.clear();
- this.sequence = 0;
- }
+ // Private
- onNativeMessage({sequence, data}) {
- const listener = this.listeners.get(sequence);
- if (typeof listener === 'undefined') { return; }
+ _onMessage({sequence, data}) {
+ const invocation = this._invocations.get(sequence);
+ if (typeof invocation === 'undefined') { return; }
- const {callback, timer} = listener;
+ const {resolve, timer} = invocation;
clearTimeout(timer);
- callback(data);
- this.listeners.delete(sequence);
+ resolve(data);
+ this._invocations.delete(sequence);
}
- invoke(action, params) {
- if (this.port === null) {
- return Promise.resolve({});
+ _onDisconnect() {
+ if (this._port === null) { return; }
+ const e = chrome.runtime.lastError;
+ const error = new Error(e ? e.message : 'MeCab disconnected');
+ for (const {reject, timer} of this._invocations.values()) {
+ clearTimeout(timer);
+ reject(error);
}
+ this._clearPort();
+ }
+
+ _invoke(action, params) {
return new Promise((resolve, reject) => {
- const sequence = this.sequence++;
+ if (this._port === null) {
+ reject(new Error('Port disconnected'));
+ }
- this.listeners.set(sequence, {
- callback: resolve,
- timer: setTimeout(() => {
- this.listeners.delete(sequence);
- reject(new Error(`Mecab invoke timed out in ${Mecab.timeout} ms`));
- }, Mecab.timeout)
- });
+ const sequence = this._sequence++;
- this.port.postMessage({action, params, sequence});
+ const timer = setTimeout(() => {
+ this._invocations.delete(sequence);
+ reject(new Error(`MeCab invoke timed out after ${this._timeout}ms`));
+ }, this._timeout);
+
+ this._invocations.set(sequence, {resolve, reject, timer}, this._timeout);
+
+ this._port.postMessage({action, params, sequence});
});
}
-}
-Mecab.timeout = 5000;
-Mecab.version = 1;
+ _convertParseTextResults(rawResults) {
+ const results = [];
+ for (const [name, rawLines] of Object.entries(rawResults)) {
+ const lines = [];
+ for (const rawLine of rawLines) {
+ const line = [];
+ for (let {expression, reading, source} of rawLine) {
+ if (typeof expression !== 'string') { expression = ''; }
+ if (typeof reading !== 'string') { reading = ''; }
+ if (typeof source !== 'string') { source = ''; }
+ line.push({expression, reading, source});
+ }
+ lines.push(line);
+ }
+ results.push({name, lines});
+ }
+ return results;
+ }
+
+ async _setupPort() {
+ if (!this._enabled) {
+ throw new Error('MeCab not enabled');
+ }
+ if (this._setupPortPromise === null) {
+ this._setupPortPromise = this._setupPort2();
+ }
+ try {
+ await this._setupPortPromise;
+ } catch (e) {
+ throw new Error(e.message);
+ }
+ }
+
+ async _setupPort2() {
+ const port = chrome.runtime.connectNative('yomichan_mecab');
+ this._eventListeners.addListener(port.onMessage, this._onMessage.bind(this));
+ this._eventListeners.addListener(port.onDisconnect, this._onDisconnect.bind(this));
+ this._port = port;
+
+ try {
+ const {version} = await this._invoke('get_version', {});
+ this._remoteVersion = version;
+ if (version !== this._version) {
+ throw new Error(`Unsupported MeCab native messenger version ${version}. Yomichan supports version ${this._version}.`);
+ }
+ } catch (e) {
+ if (this._port === port) {
+ this._clearPort();
+ }
+ throw e;
+ }
+ }
+
+ _clearPort() {
+ this._port.disconnect();
+ this._port = null;
+ this._invocations.clear();
+ this._eventListeners.removeAllEventListeners();
+ this._sequence = 0;
+ this._setupPortPromise = null;
+ }
+}