This commit is contained in:
Alex Yatskov 2020-04-16 17:30:09 -07:00
commit 93c4fb9eab
28 changed files with 745 additions and 475 deletions

View File

@ -388,7 +388,8 @@
"convertNumericCharacters", "convertNumericCharacters",
"convertAlphabeticCharacters", "convertAlphabeticCharacters",
"convertHiraganaToKatakana", "convertHiraganaToKatakana",
"convertKatakanaToHiragana" "convertKatakanaToHiragana",
"collapseEmphaticSequences"
], ],
"properties": { "properties": {
"convertHalfWidthCharacters": { "convertHalfWidthCharacters": {
@ -415,6 +416,11 @@
"type": "string", "type": "string",
"enum": ["false", "true", "variant"], "enum": ["false", "true", "variant"],
"default": "variant" "default": "variant"
},
"collapseEmphaticSequences": {
"type": "string",
"enum": ["false", "true", "full"],
"default": "false"
} }
} }
}, },

View File

@ -30,7 +30,6 @@
* Translator * Translator
* conditionsTestValue * conditionsTestValue
* dictConfigured * dictConfigured
* dictEnabledSet
* dictTermsSort * dictTermsSort
* handlebarsRenderDynamic * handlebarsRenderDynamic
* jp * jp
@ -76,33 +75,32 @@ class Backend {
this.messageToken = yomichan.generateId(16); this.messageToken = yomichan.generateId(16);
this._messageHandlers = new Map([ this._messageHandlers = new Map([
['yomichanCoreReady', this._onApiYomichanCoreReady.bind(this)], ['yomichanCoreReady', {handler: this._onApiYomichanCoreReady.bind(this), async: false}],
['optionsSchemaGet', this._onApiOptionsSchemaGet.bind(this)], ['optionsSchemaGet', {handler: this._onApiOptionsSchemaGet.bind(this), async: false}],
['optionsGet', this._onApiOptionsGet.bind(this)], ['optionsGet', {handler: this._onApiOptionsGet.bind(this), async: false}],
['optionsGetFull', this._onApiOptionsGetFull.bind(this)], ['optionsGetFull', {handler: this._onApiOptionsGetFull.bind(this), async: false}],
['optionsSet', this._onApiOptionsSet.bind(this)], ['optionsSet', {handler: this._onApiOptionsSet.bind(this), async: true}],
['optionsSave', this._onApiOptionsSave.bind(this)], ['optionsSave', {handler: this._onApiOptionsSave.bind(this), async: true}],
['kanjiFind', this._onApiKanjiFind.bind(this)], ['kanjiFind', {handler: this._onApiKanjiFind.bind(this), async: true}],
['termsFind', this._onApiTermsFind.bind(this)], ['termsFind', {handler: this._onApiTermsFind.bind(this), async: true}],
['textParse', this._onApiTextParse.bind(this)], ['textParse', {handler: this._onApiTextParse.bind(this), async: true}],
['textParseMecab', this._onApiTextParseMecab.bind(this)], ['definitionAdd', {handler: this._onApiDefinitionAdd.bind(this), async: true}],
['definitionAdd', this._onApiDefinitionAdd.bind(this)], ['definitionsAddable', {handler: this._onApiDefinitionsAddable.bind(this), async: true}],
['definitionsAddable', this._onApiDefinitionsAddable.bind(this)], ['noteView', {handler: this._onApiNoteView.bind(this), async: true}],
['noteView', this._onApiNoteView.bind(this)], ['templateRender', {handler: this._onApiTemplateRender.bind(this), async: true}],
['templateRender', this._onApiTemplateRender.bind(this)], ['commandExec', {handler: this._onApiCommandExec.bind(this), async: false}],
['commandExec', this._onApiCommandExec.bind(this)], ['audioGetUri', {handler: this._onApiAudioGetUri.bind(this), async: true}],
['audioGetUri', this._onApiAudioGetUri.bind(this)], ['screenshotGet', {handler: this._onApiScreenshotGet.bind(this), async: true}],
['screenshotGet', this._onApiScreenshotGet.bind(this)], ['broadcastTab', {handler: this._onApiBroadcastTab.bind(this), async: false}],
['forward', this._onApiForward.bind(this)], ['frameInformationGet', {handler: this._onApiFrameInformationGet.bind(this), async: true}],
['frameInformationGet', this._onApiFrameInformationGet.bind(this)], ['injectStylesheet', {handler: this._onApiInjectStylesheet.bind(this), async: true}],
['injectStylesheet', this._onApiInjectStylesheet.bind(this)], ['getEnvironmentInfo', {handler: this._onApiGetEnvironmentInfo.bind(this), async: true}],
['getEnvironmentInfo', this._onApiGetEnvironmentInfo.bind(this)], ['clipboardGet', {handler: this._onApiClipboardGet.bind(this), async: true}],
['clipboardGet', this._onApiClipboardGet.bind(this)], ['getDisplayTemplatesHtml', {handler: this._onApiGetDisplayTemplatesHtml.bind(this), async: true}],
['getDisplayTemplatesHtml', this._onApiGetDisplayTemplatesHtml.bind(this)], ['getQueryParserTemplatesHtml', {handler: this._onApiGetQueryParserTemplatesHtml.bind(this), async: true}],
['getQueryParserTemplatesHtml', this._onApiGetQueryParserTemplatesHtml.bind(this)], ['getZoom', {handler: this._onApiGetZoom.bind(this), async: true}],
['getZoom', this._onApiGetZoom.bind(this)], ['getMessageToken', {handler: this._onApiGetMessageToken.bind(this), async: false}],
['getMessageToken', this._onApiGetMessageToken.bind(this)], ['getDefaultAnkiFieldTemplates', {handler: this._onApiGetDefaultAnkiFieldTemplates.bind(this), async: false}]
['getDefaultAnkiFieldTemplates', this._onApiGetDefaultAnkiFieldTemplates.bind(this)]
]); ]);
this._commandHandlers = new Map([ this._commandHandlers = new Map([
@ -166,16 +164,23 @@ class Backend {
} }
onMessage({action, params}, sender, callback) { onMessage({action, params}, sender, callback) {
const handler = this._messageHandlers.get(action); const messageHandler = this._messageHandlers.get(action);
if (typeof handler !== 'function') { return false; } if (typeof messageHandler === 'undefined') { return false; }
const {handler, async} = messageHandler;
try { try {
const promise = handler(params, sender); const promiseOrResult = handler(params, sender);
promise.then( if (async) {
promiseOrResult.then(
(result) => callback({result}), (result) => callback({result}),
(error) => callback({error: errorToJson(error)}) (error) => callback({error: errorToJson(error)})
); );
return true; return true;
} else {
callback({result: promiseOrResult});
return false;
}
} catch (error) { } catch (error) {
callback({error: errorToJson(error)}); callback({error: errorToJson(error)});
return false; return false;
@ -308,31 +313,84 @@ class Backend {
return await this.dictionaryImporter.import(this.database, archiveSource, onProgress, details); return await this.dictionaryImporter.import(this.database, archiveSource, onProgress, details);
} }
async _textParseScanning(text, options) {
const results = [];
while (text.length > 0) {
const term = [];
const [definitions, sourceLength] = await this.translator.findTerms(
'simple',
text.substring(0, options.scanning.length),
{},
options
);
if (definitions.length > 0 && sourceLength > 0) {
dictTermsSort(definitions);
const {expression, reading} = definitions[0];
const source = text.substring(0, sourceLength);
for (const {text: text2, furigana} of jp.distributeFuriganaInflected(expression, reading, source)) {
const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode);
term.push({text: text2, reading: reading2});
}
text = text.substring(source.length);
} else {
const reading = jp.convertReading(text[0], '', options.parsing.readingMode);
term.push({text: text[0], reading});
text = text.substring(1);
}
results.push(term);
}
return results;
}
async _textParseMecab(text, options) {
const results = [];
const rawResults = await this.mecab.parseText(text);
for (const [mecabName, parsedLines] of Object.entries(rawResults)) {
const result = [];
for (const parsedLine of parsedLines) {
for (const {expression, reading, source} of parsedLine) {
const term = [];
for (const {text: text2, furigana} of jp.distributeFuriganaInflected(
expression.length > 0 ? expression : source,
jp.convertKatakanaToHiragana(reading),
source
)) {
const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode);
term.push({text: text2, reading: reading2});
}
result.push(term);
}
result.push([{text: '\n', reading: ''}]);
}
results.push([mecabName, result]);
}
return results;
}
// Message handlers // Message handlers
_onApiYomichanCoreReady(_params, sender) { _onApiYomichanCoreReady(_params, sender) {
// tab ID isn't set in background (e.g. browser_action) // tab ID isn't set in background (e.g. browser_action)
if (typeof sender.tab === 'undefined') {
const callback = () => this.checkLastError(chrome.runtime.lastError); const callback = () => this.checkLastError(chrome.runtime.lastError);
chrome.runtime.sendMessage({action: 'backendPrepared'}, callback); const data = {action: 'backendPrepared'};
return Promise.resolve(); if (typeof sender.tab === 'undefined') {
chrome.runtime.sendMessage(data, callback);
return false;
} else {
chrome.tabs.sendMessage(sender.tab.id, data, callback);
return true;
}
} }
const tabId = sender.tab.id; _onApiOptionsSchemaGet() {
return new Promise((resolve) => {
chrome.tabs.sendMessage(tabId, {action: 'backendPrepared'}, resolve);
});
}
async _onApiOptionsSchemaGet() {
return this.getOptionsSchema(); return this.getOptionsSchema();
} }
async _onApiOptionsGet({optionsContext}) { _onApiOptionsGet({optionsContext}) {
return this.getOptions(optionsContext); return this.getOptions(optionsContext);
} }
async _onApiOptionsGetFull() { _onApiOptionsGetFull() {
return this.getFullOptions(); return this.getFullOptions();
} }
@ -400,61 +458,27 @@ class Backend {
async _onApiTextParse({text, optionsContext}) { async _onApiTextParse({text, optionsContext}) {
const options = this.getOptions(optionsContext); const options = this.getOptions(optionsContext);
const results = []; const results = [];
while (text.length > 0) {
const term = []; if (options.parsing.enableScanningParser) {
const [definitions, sourceLength] = await this.translator.findTerms( results.push({
'simple', source: 'scanning-parser',
text.substring(0, options.scanning.length), id: 'scan',
{}, content: await this._textParseScanning(text, options)
options });
);
if (definitions.length > 0) {
dictTermsSort(definitions);
const {expression, reading} = definitions[0];
const source = text.substring(0, sourceLength);
for (const {text: text2, furigana} of jp.distributeFuriganaInflected(expression, reading, source)) {
const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode);
term.push({text: text2, reading: reading2});
}
text = text.substring(source.length);
} else {
const reading = jp.convertReading(text[0], null, options.parsing.readingMode);
term.push({text: text[0], reading});
text = text.substring(1);
}
results.push(term);
}
return results;
} }
async _onApiTextParseMecab({text, optionsContext}) { if (options.parsing.enableMecabParser) {
const options = this.getOptions(optionsContext); const mecabResults = await this._textParseMecab(text, options);
const results = []; for (const [mecabDictName, mecabDictResults] of mecabResults) {
const rawResults = await this.mecab.parseText(text); results.push({
for (const [mecabName, parsedLines] of Object.entries(rawResults)) { source: 'mecab',
const result = []; dictionary: mecabDictName,
for (const parsedLine of parsedLines) { id: `mecab-${mecabDictName}`,
for (const {expression, reading, source} of parsedLine) { content: mecabDictResults
const term = []; });
if (expression !== null && reading !== null) {
for (const {text: text2, furigana} of jp.distributeFuriganaInflected(
expression,
jp.convertKatakanaToHiragana(reading),
source
)) {
const reading2 = jp.convertReading(text2, furigana, options.parsing.readingMode);
term.push({text: text2, reading: reading2});
} }
} else {
const reading2 = jp.convertReading(source, null, options.parsing.readingMode);
term.push({text: source, reading: reading2});
}
result.push(term);
}
result.push([{text: '\n'}]);
}
results.push([mecabName, result]);
} }
return results; return results;
} }
@ -539,7 +563,7 @@ class Backend {
return this._renderTemplate(template, data); return this._renderTemplate(template, data);
} }
async _onApiCommandExec({command, params}) { _onApiCommandExec({command, params}) {
return this._runCommand(command, params); return this._runCommand(command, params);
} }
@ -559,15 +583,15 @@ class Backend {
}); });
} }
_onApiForward({action, params}, sender) { _onApiBroadcastTab({action, params}, sender) {
if (!(sender && sender.tab)) { if (!(sender && sender.tab)) {
return Promise.resolve(); return false;
} }
const tabId = sender.tab.id; const tabId = sender.tab.id;
return new Promise((resolve) => { const callback = () => this.checkLastError(chrome.runtime.lastError);
chrome.tabs.sendMessage(tabId, {action, params}, (response) => resolve(response)); chrome.tabs.sendMessage(tabId, {action, params}, callback);
}); return true;
} }
_onApiFrameInformationGet(params, sender) { _onApiFrameInformationGet(params, sender) {
@ -690,11 +714,11 @@ class Backend {
}); });
} }
async _onApiGetMessageToken() { _onApiGetMessageToken() {
return this.messageToken; return this.messageToken;
} }
async _onApiGetDefaultAnkiFieldTemplates() { _onApiGetDefaultAnkiFieldTemplates() {
return this.defaultAnkiFieldTemplates; return this.defaultAnkiFieldTemplates;
} }

View File

@ -16,10 +16,7 @@
*/ */
/* global /* global
* JSZip
* JsonSchema
* dictFieldSplit * dictFieldSplit
* requestJson
*/ */
class Database { class Database {

View File

@ -82,6 +82,9 @@
const ITERATION_MARK_CODE_POINT = 0x3005; const ITERATION_MARK_CODE_POINT = 0x3005;
const HIRAGANA_SMALL_TSU_CODE_POINT = 0x3063;
const KATAKANA_SMALL_TSU_CODE_POINT = 0x30c3;
const KANA_PROLONGED_SOUND_MARK_CODE_POINT = 0x30fc;
// Existing functions // Existing functions
@ -121,25 +124,25 @@
return wanakana.toRomaji(text); return wanakana.toRomaji(text);
} }
function convertReading(expressionFragment, readingFragment, readingMode) { function convertReading(expression, reading, readingMode) {
switch (readingMode) { switch (readingMode) {
case 'hiragana': case 'hiragana':
return convertKatakanaToHiragana(readingFragment || ''); return convertKatakanaToHiragana(reading);
case 'katakana': case 'katakana':
return convertHiraganaToKatakana(readingFragment || ''); return convertHiraganaToKatakana(reading);
case 'romaji': case 'romaji':
if (readingFragment) { if (reading) {
return convertToRomaji(readingFragment); return convertToRomaji(reading);
} else { } else {
if (isStringEntirelyKana(expressionFragment)) { if (isStringEntirelyKana(expression)) {
return convertToRomaji(expressionFragment); return convertToRomaji(expression);
} }
} }
return readingFragment; return reading;
case 'none': case 'none':
return null; return '';
default: default:
return readingFragment; return reading;
} }
} }
@ -297,7 +300,7 @@
const readingLeft = reading2.substring(group.text.length); const readingLeft = reading2.substring(group.text.length);
const segs = segmentize(readingLeft, groups.splice(1)); const segs = segmentize(readingLeft, groups.splice(1));
if (segs) { if (segs) {
return [{text: group.text}].concat(segs); return [{text: group.text, furigana: ''}].concat(segs);
} }
} }
} else { } else {
@ -365,13 +368,47 @@
} }
if (stemLength !== source.length) { if (stemLength !== source.length) {
output.push({text: source.substring(stemLength)}); output.push({text: source.substring(stemLength), furigana: ''});
} }
return output; return output;
} }
// Miscellaneous
function collapseEmphaticSequences(text, fullCollapse, sourceMap=null) {
let result = '';
let collapseCodePoint = -1;
const hasSourceMap = (sourceMap !== null);
for (const char of text) {
const c = char.codePointAt(0);
if (
c === HIRAGANA_SMALL_TSU_CODE_POINT ||
c === KATAKANA_SMALL_TSU_CODE_POINT ||
c === KANA_PROLONGED_SOUND_MARK_CODE_POINT
) {
if (collapseCodePoint !== c) {
collapseCodePoint = c;
if (!fullCollapse) {
result += char;
continue;
}
}
} else {
collapseCodePoint = -1;
result += char;
continue;
}
if (hasSourceMap) {
sourceMap.combine(Math.max(0, result.length - 1), 1);
}
}
return result;
}
// Exports // Exports
Object.assign(jp, { Object.assign(jp, {
@ -383,6 +420,7 @@
convertHalfWidthKanaToFullWidth, convertHalfWidthKanaToFullWidth,
convertAlphabeticToKana, convertAlphabeticToKana,
distributeFurigana, distributeFurigana,
distributeFuriganaInflected distributeFuriganaInflected,
collapseEmphaticSequences
}); });
})(); })();

View File

@ -40,7 +40,36 @@ class Mecab {
} }
async parseText(text) { async parseText(text) {
return await this.invoke('parse_text', {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;
}
return results;
} }
startListener() { startListener() {

View File

@ -170,7 +170,8 @@ function profileOptionsCreateDefaults() {
convertNumericCharacters: 'false', convertNumericCharacters: 'false',
convertAlphabeticCharacters: 'false', convertAlphabeticCharacters: 'false',
convertHiraganaToKatakana: 'false', convertHiraganaToKatakana: 'false',
convertKatakanaToHiragana: 'variant' convertKatakanaToHiragana: 'variant',
collapseEmphaticSequences: 'false'
}, },
dictionaries: {}, dictionaries: {},

View File

@ -19,18 +19,7 @@
* apiOptionsGet * apiOptionsGet
*/ */
async function searchFrontendSetup() { function injectSearchFrontend() {
await yomichan.prepare();
const optionsContext = {
depth: 0,
url: window.location.href
};
const options = await apiOptionsGet(optionsContext);
if (!options.scanning.enableOnSearchPage) { return; }
window.frontendInitializationData = {depth: 1, proxy: false};
const scriptSrcs = [ const scriptSrcs = [
'/mixed/js/text-scanner.js', '/mixed/js/text-scanner.js',
'/fg/js/frontend-api-receiver.js', '/fg/js/frontend-api-receiver.js',
@ -62,4 +51,29 @@ async function searchFrontendSetup() {
} }
} }
searchFrontendSetup(); async function main() {
await yomichan.prepare();
let optionsApplied = false;
const applyOptions = async () => {
const optionsContext = {
depth: 0,
url: window.location.href
};
const options = await apiOptionsGet(optionsContext);
if (!options.scanning.enableOnSearchPage || optionsApplied) { return; }
optionsApplied = true;
window.frontendInitializationData = {depth: 1, proxy: false, isSearchPage: true};
injectSearchFrontend();
yomichan.off('optionsUpdated', applyOptions);
};
yomichan.on('optionsUpdated', applyOptions);
await applyOptions();
}
main();

View File

@ -36,7 +36,7 @@ class QueryParserGenerator {
const termContainer = this._templateHandler.instantiate(preview ? 'term-preview' : 'term'); const termContainer = this._templateHandler.instantiate(preview ? 'term-preview' : 'term');
for (const segment of term) { for (const segment of term) {
if (!segment.text.trim()) { continue; } if (!segment.text.trim()) { continue; }
if (!segment.reading || !segment.reading.trim()) { if (!segment.reading.trim()) {
termContainer.appendChild(this.createSegmentText(segment.text)); termContainer.appendChild(this.createSegmentText(segment.text));
} else { } else {
termContainer.appendChild(this.createSegment(segment)); termContainer.appendChild(this.createSegment(segment));
@ -71,7 +71,17 @@ class QueryParserGenerator {
for (const parseResult of parseResults) { for (const parseResult of parseResults) {
const optionContainer = this._templateHandler.instantiate('select-option'); const optionContainer = this._templateHandler.instantiate('select-option');
optionContainer.value = parseResult.id; optionContainer.value = parseResult.id;
optionContainer.textContent = parseResult.name; switch (parseResult.source) {
case 'scanning-parser':
optionContainer.textContent = 'Scanning parser';
break;
case 'mecab':
optionContainer.textContent = `MeCab: ${parseResult.dictionary}`;
break;
default:
optionContainer.textContent = 'Unrecognized dictionary';
break;
}
optionContainer.defaultSelected = selectedParser === parseResult.id; optionContainer.defaultSelected = selectedParser === parseResult.id;
selectContainer.appendChild(optionContainer); selectContainer.appendChild(optionContainer);
} }

View File

@ -21,13 +21,12 @@
* apiOptionsSet * apiOptionsSet
* apiTermsFind * apiTermsFind
* apiTextParse * apiTextParse
* apiTextParseMecab
* docSentenceExtract * docSentenceExtract
*/ */
class QueryParser extends TextScanner { class QueryParser extends TextScanner {
constructor({getOptionsContext, setContent, setSpinnerVisible}) { constructor({getOptionsContext, setContent, setSpinnerVisible}) {
super(document.querySelector('#query-parser-content'), [], []); super(document.querySelector('#query-parser-content'), () => [], []);
this.getOptionsContext = getOptionsContext; this.getOptionsContext = getOptionsContext;
this.setContent = setContent; this.setContent = setContent;
@ -128,7 +127,7 @@ class QueryParser extends TextScanner {
this.setPreview(text); this.setPreview(text);
this.parseResults = await this.parseText(text); this.parseResults = await apiTextParse(text, this.getOptionsContext());
this.refreshSelectedParser(); this.refreshSelectedParser();
this.renderParserSelect(); this.renderParserSelect();
@ -137,33 +136,11 @@ class QueryParser extends TextScanner {
this.setSpinnerVisible(false); this.setSpinnerVisible(false);
} }
async parseText(text) {
const results = [];
if (this.options.parsing.enableScanningParser) {
results.push({
name: 'Scanning parser',
id: 'scan',
parsedText: await apiTextParse(text, this.getOptionsContext())
});
}
if (this.options.parsing.enableMecabParser) {
const mecabResults = await apiTextParseMecab(text, this.getOptionsContext());
for (const [mecabDictName, mecabDictResults] of mecabResults) {
results.push({
name: `MeCab: ${mecabDictName}`,
id: `mecab-${mecabDictName}`,
parsedText: mecabDictResults
});
}
}
return results;
}
setPreview(text) { setPreview(text) {
const previewTerms = []; const previewTerms = [];
for (let i = 0, ii = text.length; i < ii; i += 2) { for (let i = 0, ii = text.length; i < ii; i += 2) {
const tempText = text.substring(i, i + 2); const tempText = text.substring(i, i + 2);
previewTerms.push([{text: tempText}]); previewTerms.push([{text: tempText, reading: ''}]);
} }
this.queryParser.textContent = ''; this.queryParser.textContent = '';
this.queryParser.appendChild(this.queryParserGenerator.createParseResult(previewTerms, true)); this.queryParser.appendChild(this.queryParserGenerator.createParseResult(previewTerms, true));
@ -183,6 +160,6 @@ class QueryParser extends TextScanner {
const parseResult = this.getParseResult(); const parseResult = this.getParseResult();
this.queryParser.textContent = ''; this.queryParser.textContent = '';
if (!parseResult) { return; } if (!parseResult) { return; }
this.queryParser.appendChild(this.queryParserGenerator.createParseResult(parseResult.parsedText)); this.queryParser.appendChild(this.queryParserGenerator.createParseResult(parseResult.content));
} }
} }

View File

@ -208,7 +208,7 @@ class DisplaySearch extends Display {
onCopy() { onCopy() {
// ignore copy from search page // ignore copy from search page
this.clipboardMonitor.setPreviousText(document.getSelection().toString().trim()); this.clipboardMonitor.setPreviousText(window.getSelection().toString().trim());
} }
onExternalSearchUpdate({text}) { onExternalSearchUpdate({text}) {

View File

@ -118,6 +118,7 @@ async function formRead(options) {
options.translation.convertAlphabeticCharacters = $('#translation-convert-alphabetic-characters').val(); options.translation.convertAlphabeticCharacters = $('#translation-convert-alphabetic-characters').val();
options.translation.convertHiraganaToKatakana = $('#translation-convert-hiragana-to-katakana').val(); options.translation.convertHiraganaToKatakana = $('#translation-convert-hiragana-to-katakana').val();
options.translation.convertKatakanaToHiragana = $('#translation-convert-katakana-to-hiragana').val(); options.translation.convertKatakanaToHiragana = $('#translation-convert-katakana-to-hiragana').val();
options.translation.collapseEmphaticSequences = $('#translation-collapse-emphatic-sequences').val();
options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked'); options.parsing.enableScanningParser = $('#parsing-scan-enable').prop('checked');
options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked'); options.parsing.enableMecabParser = $('#parsing-mecab-enable').prop('checked');
@ -199,6 +200,7 @@ async function formWrite(options) {
$('#translation-convert-alphabetic-characters').val(options.translation.convertAlphabeticCharacters); $('#translation-convert-alphabetic-characters').val(options.translation.convertAlphabeticCharacters);
$('#translation-convert-hiragana-to-katakana').val(options.translation.convertHiraganaToKatakana); $('#translation-convert-hiragana-to-katakana').val(options.translation.convertHiraganaToKatakana);
$('#translation-convert-katakana-to-hiragana').val(options.translation.convertKatakanaToHiragana); $('#translation-convert-katakana-to-hiragana').val(options.translation.convertKatakanaToHiragana);
$('#translation-collapse-emphatic-sequences').val(options.translation.collapseEmphaticSequences);
$('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser); $('#parsing-scan-enable').prop('checked', options.parsing.enableScanningParser);
$('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser); $('#parsing-mecab-enable').prop('checked', options.parsing.enableMecabParser);

View File

@ -347,17 +347,27 @@ class Translator {
getAllDeinflections(text, options) { getAllDeinflections(text, options) {
const translationOptions = options.translation; const translationOptions = options.translation;
const collapseEmphaticOptions = [[false, false]];
switch (translationOptions.collapseEmphaticSequences) {
case 'true':
collapseEmphaticOptions.push([true, false]);
break;
case 'full':
collapseEmphaticOptions.push([true, false], [true, true]);
break;
}
const textOptionVariantArray = [ const textOptionVariantArray = [
Translator.getTextOptionEntryVariants(translationOptions.convertHalfWidthCharacters), Translator.getTextOptionEntryVariants(translationOptions.convertHalfWidthCharacters),
Translator.getTextOptionEntryVariants(translationOptions.convertNumericCharacters), Translator.getTextOptionEntryVariants(translationOptions.convertNumericCharacters),
Translator.getTextOptionEntryVariants(translationOptions.convertAlphabeticCharacters), Translator.getTextOptionEntryVariants(translationOptions.convertAlphabeticCharacters),
Translator.getTextOptionEntryVariants(translationOptions.convertHiraganaToKatakana), Translator.getTextOptionEntryVariants(translationOptions.convertHiraganaToKatakana),
Translator.getTextOptionEntryVariants(translationOptions.convertKatakanaToHiragana) Translator.getTextOptionEntryVariants(translationOptions.convertKatakanaToHiragana),
collapseEmphaticOptions
]; ];
const deinflections = []; const deinflections = [];
const used = new Set(); const used = new Set();
for (const [halfWidth, numeric, alphabetic, katakana, hiragana] of Translator.getArrayVariants(textOptionVariantArray)) { for (const [halfWidth, numeric, alphabetic, katakana, hiragana, [collapseEmphatic, collapseEmphaticFull]] of Translator.getArrayVariants(textOptionVariantArray)) {
let text2 = text; let text2 = text;
const sourceMap = new TextSourceMap(text2); const sourceMap = new TextSourceMap(text2);
if (halfWidth) { if (halfWidth) {
@ -375,6 +385,9 @@ class Translator {
if (hiragana) { if (hiragana) {
text2 = jp.convertKatakanaToHiragana(text2); text2 = jp.convertKatakanaToHiragana(text2);
} }
if (collapseEmphatic) {
text2 = jp.collapseEmphaticSequences(text2, collapseEmphaticFull, sourceMap);
}
for (let i = text2.length; i > 0; --i) { for (let i = text2.length; i > 0; --i) {
const text2Substring = text2.substring(0, i); const text2Substring = text2.substring(0, i);

View File

@ -427,7 +427,7 @@
<p class="help-block"> <p class="help-block">
The conversion options below are listed in the order that the conversions are applied to the input text. The conversion options below are listed in the order that the conversions are applied to the input text.
Each conversion has three possible values: Conversions commonly have three possible values:
</p> </p>
<ul class="help-block"> <ul class="help-block">
@ -490,6 +490,15 @@
<option value="variant">Use both variants</option> <option value="variant">Use both variants</option>
</select> </select>
</div> </div>
<div class="form-group">
<label for="translation-collapse-emphatic-sequences">Collapse emphatic character sequences <span class="label-light">(すっっごーーい &rarr; すっごーい / すごい)</span></label>
<select class="form-control" id="translation-collapse-emphatic-sequences">
<option value="false">Disabled</option>
<option value="true">Collapse into single character</option>
<option value="full">Remove all characters</option>
</select>
</div>
</div> </div>
<div id="popup-content-scanning"> <div id="popup-content-scanning">

View File

@ -17,7 +17,7 @@
/* global /* global
* Display * Display
* apiForward * apiBroadcastTab
* apiGetMessageToken * apiGetMessageToken
* popupNestedInitialize * popupNestedInitialize
*/ */
@ -79,7 +79,7 @@ class DisplayFloat extends Display {
this.setContentScale(scale); this.setContentScale(scale);
apiForward('popupPrepareCompleted', {targetPopupId: this._popupId}); apiBroadcastTab('popupPrepareCompleted', {targetPopupId: this._popupId});
} }
onError(error) { onError(error) {
@ -180,7 +180,7 @@ class DisplayFloat extends Display {
}, },
2000 2000
); );
apiForward('requestDocumentInformationBroadcast', {uniqueId}); apiBroadcastTab('requestDocumentInformationBroadcast', {uniqueId});
const {title} = await promise; const {title} = await promise;
return title; return title;

View File

@ -16,7 +16,7 @@
*/ */
/* global /* global
* apiForward * apiBroadcastTab
*/ */
class FrameOffsetForwarder { class FrameOffsetForwarder {
@ -96,6 +96,6 @@ class FrameOffsetForwarder {
} }
_forwardFrameOffsetOrigin(offset, uniqueId) { _forwardFrameOffsetOrigin(offset, uniqueId) {
apiForward('frameOffset', {offset, uniqueId}); apiBroadcastTab('frameOffset', {offset, uniqueId});
} }
} }

View File

@ -20,21 +20,11 @@
* Frontend * Frontend
* PopupProxy * PopupProxy
* PopupProxyHost * PopupProxyHost
* apiForward * apiBroadcastTab
* apiOptionsGet * apiOptionsGet
*/ */
async function main() { async function createIframePopupProxy(url, frameOffsetForwarder) {
await yomichan.prepare();
const data = window.frontendInitializationData || {};
const {id, depth=0, parentFrameId, url, proxy=false} = data;
const optionsContext = {depth, url};
const options = await apiOptionsGet(optionsContext);
let popup;
if (!proxy && (window !== window.parent) && options.general.showIframePopupsInRootFrame) {
const rootPopupInformationPromise = yomichan.getTemporaryListenerResult( const rootPopupInformationPromise = yomichan.getTemporaryListenerResult(
chrome.runtime.onMessage, chrome.runtime.onMessage,
({action, params}, {resolve}) => { ({action, params}, {resolve}) => {
@ -43,30 +33,92 @@ async function main() {
} }
} }
); );
apiForward('rootPopupRequestInformationBroadcast'); apiBroadcastTab('rootPopupRequestInformationBroadcast');
const {popupId, frameId} = await rootPopupInformationPromise; const {popupId, frameId} = await rootPopupInformationPromise;
const frameOffsetForwarder = new FrameOffsetForwarder();
frameOffsetForwarder.start();
const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder); const getFrameOffset = frameOffsetForwarder.getOffset.bind(frameOffsetForwarder);
popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset); const popup = new PopupProxy(popupId, 0, null, frameId, url, getFrameOffset);
await popup.prepare(); await popup.prepare();
} else if (proxy) {
popup = new PopupProxy(null, depth + 1, id, parentFrameId, url);
await popup.prepare();
} else {
const frameOffsetForwarder = new FrameOffsetForwarder();
frameOffsetForwarder.start();
return popup;
}
async function getOrCreatePopup(depth) {
const popupHost = new PopupProxyHost(); const popupHost = new PopupProxyHost();
await popupHost.prepare(); await popupHost.prepare();
popup = popupHost.getOrCreatePopup(null, null, depth); const popup = popupHost.getOrCreatePopup(null, null, depth);
return popup;
}
async function createPopupProxy(depth, id, parentFrameId, url) {
const popup = new PopupProxy(null, depth + 1, id, parentFrameId, url);
await popup.prepare();
return popup;
}
async function main() {
await yomichan.prepare();
const data = window.frontendInitializationData || {};
const {id, depth=0, parentFrameId, url=window.location.href, proxy=false, isSearchPage=false} = data;
const isIframe = !proxy && (window !== window.parent);
const popups = {
iframe: null,
proxy: null,
normal: null
};
let frontend = null;
let frontendPreparePromise = null;
let frameOffsetForwarder = null;
const applyOptions = async () => {
const optionsContext = {depth: isSearchPage ? 0 : depth, url};
const options = await apiOptionsGet(optionsContext);
if (!proxy && frameOffsetForwarder === null) {
frameOffsetForwarder = new FrameOffsetForwarder();
frameOffsetForwarder.start();
} }
const frontend = new Frontend(popup); let popup;
await frontend.prepare(); if (isIframe && options.general.showIframePopupsInRootFrame) {
popup = popups.iframe || await createIframePopupProxy(url, frameOffsetForwarder);
popups.iframe = popup;
} else if (proxy) {
popup = popups.proxy || await createPopupProxy(depth, id, parentFrameId, url);
popups.proxy = popup;
} else {
popup = popups.normal || await getOrCreatePopup(depth);
popups.normal = popup;
}
if (frontend === null) {
frontend = new Frontend(popup);
frontendPreparePromise = frontend.prepare();
await frontendPreparePromise;
} else {
await frontendPreparePromise;
if (isSearchPage) {
const disabled = !options.scanning.enableOnSearchPage;
frontend.setDisabledOverride(disabled);
}
if (isIframe) {
await frontend.setPopup(popup);
}
}
};
yomichan.on('optionsUpdated', applyOptions);
await applyOptions();
} }
main(); main();

View File

@ -17,7 +17,7 @@
/* global /* global
* TextScanner * TextScanner
* apiForward * apiBroadcastTab
* apiGetZoom * apiGetZoom
* apiKanjiFind * apiKanjiFind
* apiOptionsGet * apiOptionsGet
@ -29,11 +29,14 @@ class Frontend extends TextScanner {
constructor(popup) { constructor(popup) {
super( super(
window, window,
popup.isProxy() ? [] : [popup.getContainer()], () => this.popup.isProxy() ? [] : [this.popup.getContainer()],
[(x, y) => this.popup.containsPoint(x, y)] [(x, y) => this.popup.containsPoint(x, y)]
); );
this.popup = popup; this.popup = popup;
this._disabledOverride = false;
this.options = null; this.options = null;
this.optionsContext = { this.optionsContext = {
@ -43,7 +46,7 @@ class Frontend extends TextScanner {
this._pageZoomFactor = 1.0; this._pageZoomFactor = 1.0;
this._contentScale = 1.0; this._contentScale = 1.0;
this._orphaned = true; this._orphaned = false;
this._lastShowPromise = Promise.resolve(); this._lastShowPromise = Promise.resolve();
this._windowMessageHandlers = new Map([ this._windowMessageHandlers = new Map([
@ -132,8 +135,20 @@ class Frontend extends TextScanner {
]; ];
} }
setDisabledOverride(disabled) {
this._disabledOverride = disabled;
this.setEnabled(this.options.general.enable, this._canEnable());
}
async setPopup(popup) {
this.onSearchClear(false);
this.popup = popup;
await popup.setOptions(this.options);
}
async updateOptions() { async updateOptions() {
this.setOptions(await apiOptionsGet(this.getOptionsContext())); this.options = await apiOptionsGet(this.getOptionsContext());
this.setOptions(this.options, this._canEnable());
const ignoreNodes = ['.scan-disable', '.scan-disable *']; const ignoreNodes = ['.scan-disable', '.scan-disable *'];
if (!this.options.scanning.enableOnPopupExpressions) { if (!this.options.scanning.enableOnPopupExpressions) {
@ -259,19 +274,23 @@ class Frontend extends TextScanner {
} }
_broadcastRootPopupInformation() { _broadcastRootPopupInformation() {
if (!this.popup.isProxy() && this.popup.depth === 0) { if (!this.popup.isProxy() && this.popup.depth === 0 && this.popup.frameId === 0) {
apiForward('rootPopupInformation', {popupId: this.popup.id, frameId: this.popup.frameId}); apiBroadcastTab('rootPopupInformation', {popupId: this.popup.id, frameId: this.popup.frameId});
} }
} }
_broadcastDocumentInformation(uniqueId) { _broadcastDocumentInformation(uniqueId) {
apiForward('documentInformationBroadcast', { apiBroadcastTab('documentInformationBroadcast', {
uniqueId, uniqueId,
frameId: this.popup.frameId, frameId: this.popup.frameId,
title: document.title title: document.title
}); });
} }
_canEnable() {
return this.popup.depth <= this.options.scanning.popupNestingMaxDepth && !this._disabledOverride;
}
async _updatePopupPosition() { async _updatePopupPosition() {
const textSource = this.getCurrentTextSource(); const textSource = this.getCurrentTextSource();
if (textSource !== null && await this.popup.isVisible()) { if (textSource !== null && await this.popup.isVisible()) {

View File

@ -19,24 +19,7 @@
* apiOptionsGet * apiOptionsGet
*/ */
let popupNestedInitialized = false; function injectPopupNested() {
async function popupNestedInitialize(id, depth, parentFrameId, url) {
if (popupNestedInitialized) {
return;
}
popupNestedInitialized = true;
const optionsContext = {depth, url};
const options = await apiOptionsGet(optionsContext);
const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth;
if (!(typeof popupNestingMaxDepth === 'number' && typeof depth === 'number' && depth < popupNestingMaxDepth)) {
return;
}
window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true};
const scriptSrcs = [ const scriptSrcs = [
'/mixed/js/text-scanner.js', '/mixed/js/text-scanner.js',
'/fg/js/frontend-api-sender.js', '/fg/js/frontend-api-sender.js',
@ -52,3 +35,33 @@ async function popupNestedInitialize(id, depth, parentFrameId, url) {
document.body.appendChild(script); document.body.appendChild(script);
} }
} }
async function popupNestedInitialize(id, depth, parentFrameId, url) {
let optionsApplied = false;
const applyOptions = async () => {
const optionsContext = {depth, url};
const options = await apiOptionsGet(optionsContext);
const popupNestingMaxDepth = options.scanning.popupNestingMaxDepth;
const maxPopupDepthExceeded = !(
typeof popupNestingMaxDepth === 'number' &&
typeof depth === 'number' &&
depth < popupNestingMaxDepth
);
if (maxPopupDepthExceeded || optionsApplied) {
return;
}
optionsApplied = true;
window.frontendInitializationData = {id, depth, parentFrameId, url, proxy: true};
injectPopupNested();
yomichan.off('optionsUpdated', applyOptions);
};
yomichan.on('optionsUpdated', applyOptions);
await applyOptions();
}

View File

@ -297,13 +297,13 @@ button.action-button {
content: "\3001"; content: "\3001";
} }
.term-expression-list[data-multi=true]>.term-expression:last-of-type:after { .entry[data-expression-multi=true] .term-expression-list>.term-expression:last-of-type:after {
font-size: 2em; font-size: 2em;
content: "\3000"; content: "\3000";
visibility: hidden; visibility: hidden;
} }
.term-expression-list[data-multi=true] .term-expression-details { .entry[data-expression-multi=true] .term-expression-list .term-expression-details {
display: inline-block; display: inline-block;
position: relative; position: relative;
width: 0; width: 0;
@ -312,21 +312,21 @@ button.action-button {
z-index: 1; z-index: 1;
} }
.term-expression-list[data-multi=true] .term-expression:hover .term-expression-details { .entry[data-expression-multi=true] .term-expression:hover .term-expression-details {
visibility: visible; visibility: visible;
} }
.term-expression-list[data-multi=true] .term-expression-details>.action-play-audio { .entry[data-expression-multi=true] .term-expression-list .term-expression-details>.action-play-audio {
position: absolute; position: absolute;
left: 0; left: 0;
bottom: 0.5em; bottom: 0.5em;
} }
.term-expression-list:not([data-multi=true]) .term-expression-details>.action-play-audio { .entry:not([data-expression-multi=true]) .term-expression-list .term-expression-details>.action-play-audio {
display: none; display: none;
} }
.term-expression-list[data-multi=true] .term-expression-details>.tags { .entry[data-expression-multi=true] .term-expression-list .term-expression-details>.tags {
display: block; display: block;
position: absolute; position: absolute;
left: 0; left: 0;
@ -334,7 +334,7 @@ button.action-button {
white-space: nowrap; white-space: nowrap;
} }
.term-expression-list[data-multi=true] .term-expression-details>.frequencies { .entry[data-expression-multi=true] .term-expression-list .term-expression-details>.frequencies {
display: block; display: block;
position: absolute; position: absolute;
left: 0; left: 0;
@ -364,19 +364,19 @@ button.action-button {
list-style-type: circle; list-style-type: circle;
} }
.term-definition-only-list[data-count="0"] { .term-definition-disambiguation-list[data-count="0"] {
display: none; display: none;
} }
.term-definition-only-list:before { .term-definition-disambiguation-list:before {
content: "("; content: "(";
} }
.term-definition-only-list:after { .term-definition-disambiguation-list:after {
content: " only)"; content: " only)";
} }
.term-definition-only+.term-definition-only:before { .term-definition-disambiguation+.term-definition-disambiguation:before {
content: ", "; content: ", ";
} }
@ -398,7 +398,7 @@ button.action-button {
} }
:root[data-compact-glossaries=true] .term-definition-tag-list, :root[data-compact-glossaries=true] .term-definition-tag-list,
:root[data-compact-glossaries=true] .term-definition-only-list:not([data-count="0"]) { :root[data-compact-glossaries=true] .term-definition-disambiguation-list:not([data-count="0"]) {
display: inline; display: inline;
} }

View File

@ -30,10 +30,10 @@
</div></div></template> </div></div></template>
<template id="term-definition-item-template"><li class="term-definition-item"> <template id="term-definition-item-template"><li class="term-definition-item">
<div class="term-definition-tag-list tag-list"></div> <div class="term-definition-tag-list tag-list"></div>
<div class="term-definition-only-list"></div> <div class="term-definition-disambiguation-list"></div>
<ul class="term-glossary-list"></ul> <ul class="term-glossary-list"></ul>
</li></template> </li></template>
<template id="term-definition-only-template"><span class="term-definition-only"></span></template> <template id="term-definition-disambiguation-template"><span class="term-definition-disambiguation"></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-glossary-item-template"><li class="term-glossary-item"><span class="term-glossary-separator"> </span><span class="term-glossary"></span></li></template>
<template id="term-reason-template"><span class="term-reason"></span><span class="term-reason-separator"> </span></template> <template id="term-reason-template"><span class="term-reason"></span><span class="term-reason-separator"> </span></template>

View File

@ -44,10 +44,6 @@ function apiTextParse(text, optionsContext) {
return _apiInvoke('textParse', {text, optionsContext}); return _apiInvoke('textParse', {text, optionsContext});
} }
function apiTextParseMecab(text, optionsContext) {
return _apiInvoke('textParseMecab', {text, optionsContext});
}
function apiKanjiFind(text, optionsContext) { function apiKanjiFind(text, optionsContext) {
return _apiInvoke('kanjiFind', {text, optionsContext}); return _apiInvoke('kanjiFind', {text, optionsContext});
} }
@ -80,8 +76,8 @@ function apiScreenshotGet(options) {
return _apiInvoke('screenshotGet', {options}); return _apiInvoke('screenshotGet', {options});
} }
function apiForward(action, params) { function apiBroadcastTab(action, params) {
return _apiInvoke('forward', {action, params}); return _apiInvoke('broadcastTab', {action, params});
} }
function apiFrameInformationGet() { function apiFrameInformationGet() {

View File

@ -43,13 +43,15 @@ class DisplayGenerator {
const debugInfoContainer = node.querySelector('.debug-info'); const debugInfoContainer = node.querySelector('.debug-info');
const bodyContainer = node.querySelector('.term-entry-body'); const bodyContainer = node.querySelector('.term-entry-body');
const pitches = DisplayGenerator._getPitchInfos(details); const {termTags, expressions, definitions} = details;
const pitches = this._getPitchInfos(details);
const pitchCount = pitches.reduce((i, v) => i + v[1].length, 0); const pitchCount = pitches.reduce((i, v) => i + v[1].length, 0);
const expressionMulti = Array.isArray(details.expressions); const expressionMulti = Array.isArray(expressions);
const definitionMulti = Array.isArray(details.definitions); const definitionMulti = Array.isArray(definitions);
const expressionCount = expressionMulti ? details.expressions.length : 1; const expressionCount = expressionMulti ? expressions.length : 1;
const definitionCount = definitionMulti ? details.definitions.length : 1; const definitionCount = definitionMulti ? definitions.length : 1;
const uniqueExpressionCount = Array.isArray(details.expression) ? new Set(details.expression).size : 1; const uniqueExpressionCount = Array.isArray(details.expression) ? new Set(details.expression).size : 1;
node.dataset.expressionMulti = `${expressionMulti}`; node.dataset.expressionMulti = `${expressionMulti}`;
@ -65,15 +67,11 @@ class DisplayGenerator {
(pitches.length > 0 ? 1 : 0) (pitches.length > 0 ? 1 : 0)
}`; }`;
const termTags = details.termTags; this._appendMultiple(expressionsContainer, this._createTermExpression.bind(this), expressionMulti ? expressions : [details], termTags);
let expressions = details.expressions; this._appendMultiple(reasonsContainer, this._createTermReason.bind(this), details.reasons);
expressions = Array.isArray(expressions) ? expressions.map((e) => [e, termTags]) : null; this._appendMultiple(frequenciesContainer, this._createFrequencyTag.bind(this), details.frequencies);
this._appendMultiple(pitchesContainer, this._createPitches.bind(this), pitches);
DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), expressions, [[details, termTags]]); this._appendMultiple(definitionsContainer, this._createTermDefinitionItem.bind(this), definitionMulti ? definitions : [details]);
DisplayGenerator._appendMultiple(reasonsContainer, this.createTermReason.bind(this), details.reasons);
DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies);
DisplayGenerator._appendMultiple(pitchesContainer, this.createPitches.bind(this), pitches);
DisplayGenerator._appendMultiple(definitionsContainer, this.createTermDefinitionItem.bind(this), details.definitions, [details]);
if (debugInfoContainer !== null) { if (debugInfoContainer !== null) {
debugInfoContainer.textContent = JSON.stringify(details, null, 4); debugInfoContainer.textContent = JSON.stringify(details, null, 4);
@ -82,88 +80,6 @@ class DisplayGenerator {
return node; return node;
} }
createTermExpression([details, termTags]) {
const node = this._templateHandler.instantiate('term-expression');
const expressionContainer = node.querySelector('.term-expression-text');
const tagContainer = node.querySelector('.tags');
const frequencyContainer = node.querySelector('.frequencies');
if (details.termFrequency) {
node.dataset.frequency = details.termFrequency;
}
if (expressionContainer !== null) {
let furiganaSegments = details.furiganaSegments;
if (!Array.isArray(furiganaSegments)) {
// This case should not occur
furiganaSegments = [{text: details.expression, furigana: details.reading}];
}
DisplayGenerator._appendFurigana(expressionContainer, furiganaSegments, this._appendKanjiLinks.bind(this));
}
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 fragment = this._templateHandler.instantiateFragment('term-reason');
const node = fragment.querySelector('.term-reason');
node.textContent = reason;
node.dataset.reason = reason;
return fragment;
}
createTermDefinitionItem(details) {
const node = this._templateHandler.instantiate('term-definition-item');
const tagListContainer = node.querySelector('.term-definition-tag-list');
const onlyListContainer = node.querySelector('.term-definition-only-list');
const glossaryContainer = node.querySelector('.term-glossary-list');
node.dataset.dictionary = details.dictionary;
DisplayGenerator._appendMultiple(tagListContainer, this.createTag.bind(this), details.definitionTags);
DisplayGenerator._appendMultiple(onlyListContainer, this.createTermOnly.bind(this), details.only);
DisplayGenerator._appendMultiple(glossaryContainer, this.createTermGlossaryItem.bind(this), details.glossary);
return node;
}
createTermGlossaryItem(glossary) {
const node = this._templateHandler.instantiate('term-glossary-item');
const container = node.querySelector('.term-glossary');
if (container !== null) {
DisplayGenerator._appendMultilineText(container, glossary);
}
return node;
}
createTermOnly(only) {
const node = this._templateHandler.instantiate('term-definition-only');
node.dataset.only = only;
node.textContent = only;
return node;
}
createKanjiLink(character) {
const node = document.createElement('a');
node.href = '#';
node.className = 'kanji-link';
node.textContent = character;
return node;
}
createKanjiEntry(details) { createKanjiEntry(details) {
const node = this._templateHandler.instantiate('kanji-entry'); const node = this._templateHandler.instantiate('kanji-entry');
@ -183,23 +99,23 @@ class DisplayGenerator {
glyphContainer.textContent = details.character; glyphContainer.textContent = details.character;
} }
DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies); this._appendMultiple(frequenciesContainer, this._createFrequencyTag.bind(this), details.frequencies);
DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), details.tags); this._appendMultiple(tagContainer, this._createTag.bind(this), details.tags);
DisplayGenerator._appendMultiple(glossaryContainer, this.createKanjiGlossaryItem.bind(this), details.glossary); this._appendMultiple(glossaryContainer, this._createKanjiGlossaryItem.bind(this), details.glossary);
DisplayGenerator._appendMultiple(chineseReadingsContainer, this.createKanjiReading.bind(this), details.onyomi); this._appendMultiple(chineseReadingsContainer, this._createKanjiReading.bind(this), details.onyomi);
DisplayGenerator._appendMultiple(japaneseReadingsContainer, this.createKanjiReading.bind(this), details.kunyomi); this._appendMultiple(japaneseReadingsContainer, this._createKanjiReading.bind(this), details.kunyomi);
if (statisticsContainer !== null) { if (statisticsContainer !== null) {
statisticsContainer.appendChild(this.createKanjiInfoTable(details.stats.misc)); statisticsContainer.appendChild(this._createKanjiInfoTable(details.stats.misc));
} }
if (classificationsContainer !== null) { if (classificationsContainer !== null) {
classificationsContainer.appendChild(this.createKanjiInfoTable(details.stats.class)); classificationsContainer.appendChild(this._createKanjiInfoTable(details.stats.class));
} }
if (codepointsContainer !== null) { if (codepointsContainer !== null) {
codepointsContainer.appendChild(this.createKanjiInfoTable(details.stats.code)); codepointsContainer.appendChild(this._createKanjiInfoTable(details.stats.code));
} }
if (dictionaryIndicesContainer !== null) { if (dictionaryIndicesContainer !== null) {
dictionaryIndicesContainer.appendChild(this.createKanjiInfoTable(details.stats.index)); dictionaryIndicesContainer.appendChild(this._createKanjiInfoTable(details.stats.index));
} }
if (debugInfoContainer !== null) { if (debugInfoContainer !== null) {
@ -209,30 +125,114 @@ class DisplayGenerator {
return node; return node;
} }
createKanjiGlossaryItem(glossary) { // Private
const node = this._templateHandler.instantiate('kanji-glossary-item');
const container = node.querySelector('.kanji-glossary'); _createTermExpression(details, termTags) {
const node = this._templateHandler.instantiate('term-expression');
const expressionContainer = node.querySelector('.term-expression-text');
const tagContainer = node.querySelector('.tags');
const frequencyContainer = node.querySelector('.frequencies');
if (details.termFrequency) {
node.dataset.frequency = details.termFrequency;
}
if (expressionContainer !== null) {
let furiganaSegments = details.furiganaSegments;
if (!Array.isArray(furiganaSegments)) {
// This case should not occur
furiganaSegments = [{text: details.expression, furigana: details.reading}];
}
this._appendFurigana(expressionContainer, furiganaSegments, this._appendKanjiLinks.bind(this));
}
if (!Array.isArray(termTags)) {
// Fallback
termTags = details.termTags;
}
const searchQueries = [details.expression, details.reading]
.filter((x) => !!x)
.map((x) => ({query: x}));
this._appendMultiple(tagContainer, this._createTag.bind(this), termTags);
this._appendMultiple(tagContainer, this._createSearchTag.bind(this), searchQueries);
this._appendMultiple(frequencyContainer, this._createFrequencyTag.bind(this), details.frequencies);
return node;
}
_createTermReason(reason) {
const fragment = this._templateHandler.instantiateFragment('term-reason');
const node = fragment.querySelector('.term-reason');
node.textContent = reason;
node.dataset.reason = reason;
return fragment;
}
_createTermDefinitionItem(details) {
const node = this._templateHandler.instantiate('term-definition-item');
const tagListContainer = node.querySelector('.term-definition-tag-list');
const onlyListContainer = node.querySelector('.term-definition-disambiguation-list');
const glossaryContainer = node.querySelector('.term-glossary-list');
node.dataset.dictionary = details.dictionary;
this._appendMultiple(tagListContainer, this._createTag.bind(this), details.definitionTags);
this._appendMultiple(onlyListContainer, this._createTermDisambiguation.bind(this), details.only);
this._appendMultiple(glossaryContainer, this._createTermGlossaryItem.bind(this), details.glossary);
return node;
}
_createTermGlossaryItem(glossary) {
const node = this._templateHandler.instantiate('term-glossary-item');
const container = node.querySelector('.term-glossary');
if (container !== null) { if (container !== null) {
DisplayGenerator._appendMultilineText(container, glossary); this._appendMultilineText(container, glossary);
} }
return node; return node;
} }
createKanjiReading(reading) { _createTermDisambiguation(disambiguation) {
const node = this._templateHandler.instantiate('term-definition-disambiguation');
node.dataset.term = disambiguation;
node.textContent = disambiguation;
return node;
}
_createKanjiLink(character) {
const node = document.createElement('a');
node.href = '#';
node.className = 'kanji-link';
node.textContent = character;
return node;
}
_createKanjiGlossaryItem(glossary) {
const node = this._templateHandler.instantiate('kanji-glossary-item');
const container = node.querySelector('.kanji-glossary');
if (container !== null) {
this._appendMultilineText(container, glossary);
}
return node;
}
_createKanjiReading(reading) {
const node = this._templateHandler.instantiate('kanji-reading'); const node = this._templateHandler.instantiate('kanji-reading');
node.textContent = reading; node.textContent = reading;
return node; return node;
} }
createKanjiInfoTable(details) { _createKanjiInfoTable(details) {
const node = this._templateHandler.instantiate('kanji-info-table'); const node = this._templateHandler.instantiate('kanji-info-table');
const container = node.querySelector('.kanji-info-table-body'); const container = node.querySelector('.kanji-info-table-body');
if (container !== null) { if (container !== null) {
const count = DisplayGenerator._appendMultiple(container, this.createKanjiInfoTableItem.bind(this), details); const count = this._appendMultiple(container, this._createKanjiInfoTableItem.bind(this), details);
if (count === 0) { if (count === 0) {
const n = this.createKanjiInfoTableItemEmpty(); const n = this._createKanjiInfoTableItemEmpty();
container.appendChild(n); container.appendChild(n);
} }
} }
@ -240,7 +240,7 @@ class DisplayGenerator {
return node; return node;
} }
createKanjiInfoTableItem(details) { _createKanjiInfoTableItem(details) {
const node = this._templateHandler.instantiate('kanji-info-table-item'); const node = this._templateHandler.instantiate('kanji-info-table-item');
const nameNode = node.querySelector('.kanji-info-table-item-header'); const nameNode = node.querySelector('.kanji-info-table-item-header');
const valueNode = node.querySelector('.kanji-info-table-item-value'); const valueNode = node.querySelector('.kanji-info-table-item-value');
@ -253,11 +253,11 @@ class DisplayGenerator {
return node; return node;
} }
createKanjiInfoTableItemEmpty() { _createKanjiInfoTableItemEmpty() {
return this._templateHandler.instantiate('kanji-info-table-empty'); return this._templateHandler.instantiate('kanji-info-table-empty');
} }
createTag(details) { _createTag(details) {
const node = this._templateHandler.instantiate('tag'); const node = this._templateHandler.instantiate('tag');
const inner = node.querySelector('.tag-inner'); const inner = node.querySelector('.tag-inner');
@ -269,7 +269,7 @@ class DisplayGenerator {
return node; return node;
} }
createSearchTag(details) { _createSearchTag(details) {
const node = this._templateHandler.instantiate('tag-search'); const node = this._templateHandler.instantiate('tag-search');
node.textContent = details.query; node.textContent = details.query;
@ -279,7 +279,7 @@ class DisplayGenerator {
return node; return node;
} }
createPitches(details) { _createPitches(details) {
if (!this._termPitchAccentStaticTemplateIsSetup) { if (!this._termPitchAccentStaticTemplateIsSetup) {
this._termPitchAccentStaticTemplateIsSetup = true; this._termPitchAccentStaticTemplateIsSetup = true;
const t = this._templateHandler.instantiate('term-pitch-accent-static'); const t = this._templateHandler.instantiate('term-pitch-accent-static');
@ -293,16 +293,16 @@ class DisplayGenerator {
node.dataset.pitchesMulti = 'true'; node.dataset.pitchesMulti = 'true';
node.dataset.pitchesCount = `${dictionaryPitches.length}`; node.dataset.pitchesCount = `${dictionaryPitches.length}`;
const tag = this.createTag({notes: '', name: dictionary, category: 'pitch-accent-dictionary'}); const tag = this._createTag({notes: '', name: dictionary, category: 'pitch-accent-dictionary'});
node.querySelector('.term-pitch-accent-group-tag-list').appendChild(tag); node.querySelector('.term-pitch-accent-group-tag-list').appendChild(tag);
const n = node.querySelector('.term-pitch-accent-list'); const n = node.querySelector('.term-pitch-accent-list');
DisplayGenerator._appendMultiple(n, this.createPitch.bind(this), dictionaryPitches); this._appendMultiple(n, this._createPitch.bind(this), dictionaryPitches);
return node; return node;
} }
createPitch(details) { _createPitch(details) {
const {reading, position, tags, exclusiveExpressions, exclusiveReadings} = details; const {reading, position, tags, exclusiveExpressions, exclusiveReadings} = details;
const morae = jp.getKanaMorae(reading); const morae = jp.getKanaMorae(reading);
@ -315,10 +315,10 @@ class DisplayGenerator {
n.textContent = `${position}`; n.textContent = `${position}`;
n = node.querySelector('.term-pitch-accent-tag-list'); n = node.querySelector('.term-pitch-accent-tag-list');
DisplayGenerator._appendMultiple(n, this.createTag.bind(this), tags); this._appendMultiple(n, this._createTag.bind(this), tags);
n = node.querySelector('.term-pitch-accent-disambiguation-list'); n = node.querySelector('.term-pitch-accent-disambiguation-list');
this.createPitchAccentDisambiguations(n, exclusiveExpressions, exclusiveReadings); this._createPitchAccentDisambiguations(n, exclusiveExpressions, exclusiveReadings);
n = node.querySelector('.term-pitch-accent-characters'); n = node.querySelector('.term-pitch-accent-characters');
for (let i = 0, ii = morae.length; i < ii; ++i) { for (let i = 0, ii = morae.length; i < ii; ++i) {
@ -338,13 +338,13 @@ class DisplayGenerator {
} }
if (morae.length > 0) { if (morae.length > 0) {
this.populatePitchGraph(node.querySelector('.term-pitch-accent-graph'), position, morae); this._populatePitchGraph(node.querySelector('.term-pitch-accent-graph'), position, morae);
} }
return node; return node;
} }
createPitchAccentDisambiguations(container, exclusiveExpressions, exclusiveReadings) { _createPitchAccentDisambiguations(container, exclusiveExpressions, exclusiveReadings) {
const templateName = 'term-pitch-accent-disambiguation'; const templateName = 'term-pitch-accent-disambiguation';
for (const exclusiveExpression of exclusiveExpressions) { for (const exclusiveExpression of exclusiveExpressions) {
const node = this._templateHandler.instantiate(templateName); const node = this._templateHandler.instantiate(templateName);
@ -360,13 +360,12 @@ class DisplayGenerator {
container.appendChild(node); container.appendChild(node);
} }
container.dataset.multi = 'true';
container.dataset.count = `${exclusiveExpressions.length + exclusiveReadings.length}`; container.dataset.count = `${exclusiveExpressions.length + exclusiveReadings.length}`;
container.dataset.expressionCount = `${exclusiveExpressions.length}`; container.dataset.expressionCount = `${exclusiveExpressions.length}`;
container.dataset.readingCount = `${exclusiveReadings.length}`; container.dataset.readingCount = `${exclusiveReadings.length}`;
} }
populatePitchGraph(svg, position, morae) { _populatePitchGraph(svg, position, morae) {
const svgns = svg.getAttribute('xmlns'); const svgns = svg.getAttribute('xmlns');
const ii = morae.length; const ii = morae.length;
svg.setAttribute('viewBox', `0 0 ${50 * (ii + 1)} 100`); svg.setAttribute('viewBox', `0 0 ${50 * (ii + 1)} 100`);
@ -406,7 +405,7 @@ class DisplayGenerator {
path.setAttribute('d', `M${pathPoints.join(' L')}`); path.setAttribute('d', `M${pathPoints.join(' L')}`);
} }
createFrequencyTag(details) { _createFrequencyTag(details) {
const node = this._templateHandler.instantiate('tag-frequency'); const node = this._templateHandler.instantiate('tag-frequency');
let n = node.querySelector('.term-frequency-dictionary-name'); let n = node.querySelector('.term-frequency-dictionary-name');
@ -434,7 +433,7 @@ class DisplayGenerator {
part = ''; part = '';
} }
const link = this.createKanjiLink(c); const link = this._createKanjiLink(c);
container.appendChild(link); container.appendChild(link);
} else { } else {
part += c; part += c;
@ -445,31 +444,31 @@ class DisplayGenerator {
} }
} }
static _appendMultiple(container, createItem, detailsIterable, fallback=[]) { _isIterable(value) {
if (container === null) { return 0; } return (
value !== null &&
const multi = ( typeof value === 'object' &&
detailsIterable !== null && typeof value[Symbol.iterator] !== 'undefined'
typeof detailsIterable === 'object' &&
typeof detailsIterable[Symbol.iterator] !== 'undefined'
); );
if (!multi) { detailsIterable = fallback; } }
_appendMultiple(container, createItem, detailsIterable, ...args) {
let count = 0; let count = 0;
if (container !== null && this._isIterable(detailsIterable)) {
for (const details of detailsIterable) { for (const details of detailsIterable) {
const item = createItem(details); const item = createItem(details, ...args);
if (item === null) { continue; } if (item === null) { continue; }
container.appendChild(item); container.appendChild(item);
++count; ++count;
} }
}
container.dataset.multi = `${multi}`;
container.dataset.count = `${count}`; container.dataset.count = `${count}`;
return count; return count;
} }
static _appendFurigana(container, segments, addText) { _appendFurigana(container, segments, addText) {
for (const {text, furigana} of segments) { for (const {text, furigana} of segments) {
if (furigana) { if (furigana) {
const ruby = document.createElement('ruby'); const ruby = document.createElement('ruby');
@ -484,7 +483,7 @@ class DisplayGenerator {
} }
} }
static _appendMultilineText(container, text) { _appendMultilineText(container, text) {
const parts = text.split('\n'); const parts = text.split('\n');
container.appendChild(document.createTextNode(parts[0])); container.appendChild(document.createTextNode(parts[0]));
for (let i = 1, ii = parts.length; i < ii; ++i) { for (let i = 1, ii = parts.length; i < ii; ++i) {
@ -493,7 +492,7 @@ class DisplayGenerator {
} }
} }
static _getPitchInfos(definition) { _getPitchInfos(definition) {
const results = new Map(); const results = new Map();
const allExpressions = new Set(); const allExpressions = new Set();
@ -511,7 +510,7 @@ class DisplayGenerator {
} }
for (const {position, tags} of pitches) { for (const {position, tags} of pitches) {
let pitchInfo = DisplayGenerator._findExistingPitchInfo(reading, position, tags, dictionaryResults); let pitchInfo = this._findExistingPitchInfo(reading, position, tags, dictionaryResults);
if (pitchInfo === null) { if (pitchInfo === null) {
pitchInfo = {expressions: new Set(), reading, position, tags}; pitchInfo = {expressions: new Set(), reading, position, tags};
dictionaryResults.push(pitchInfo); dictionaryResults.push(pitchInfo);
@ -540,12 +539,12 @@ class DisplayGenerator {
return [...results.entries()]; return [...results.entries()];
} }
static _findExistingPitchInfo(reading, position, tags, pitchInfoList) { _findExistingPitchInfo(reading, position, tags, pitchInfoList) {
for (const pitchInfo of pitchInfoList) { for (const pitchInfo of pitchInfoList) {
if ( if (
pitchInfo.reading === reading && pitchInfo.reading === reading &&
pitchInfo.position === position && pitchInfo.position === position &&
DisplayGenerator._areTagListsEqual(pitchInfo.tags, tags) this._areTagListsEqual(pitchInfo.tags, tags)
) { ) {
return pitchInfo; return pitchInfo;
} }
@ -553,7 +552,7 @@ class DisplayGenerator {
return null; return null;
} }
static _areTagListsEqual(tagList1, tagList2) { _areTagListsEqual(tagList1, tagList2) {
const ii = tagList1.length; const ii = tagList1.length;
if (tagList2.length !== ii) { return false; } if (tagList2.length !== ii) { return false; }

View File

@ -22,9 +22,9 @@
* DisplayGenerator * DisplayGenerator
* WindowScroll * WindowScroll
* apiAudioGetUri * apiAudioGetUri
* apiBroadcastTab
* apiDefinitionAdd * apiDefinitionAdd
* apiDefinitionsAddable * apiDefinitionsAddable
* apiForward
* apiKanjiFind * apiKanjiFind
* apiNoteView * apiNoteView
* apiOptionsGet * apiOptionsGet
@ -854,7 +854,7 @@ class Display {
} }
setPopupVisibleOverride(visible) { setPopupVisibleOverride(visible) {
return apiForward('popupSetVisibleOverride', {visible}); return apiBroadcastTab('popupSetVisibleOverride', {visible});
} }
setSpinnerVisible(visible) { setSpinnerVisible(visible) {

View File

@ -46,7 +46,7 @@ class TextScanner {
} }
onMouseOver(e) { onMouseOver(e) {
if (this.ignoreElements.includes(e.target)) { if (this.ignoreElements().includes(e.target)) {
this.scanTimerClear(); this.scanTimerClear();
} }
} }
@ -133,7 +133,7 @@ class TextScanner {
this.preventNextClick = false; this.preventNextClick = false;
const primaryTouch = e.changedTouches[0]; const primaryTouch = e.changedTouches[0];
if (DOM.isPointInSelection(primaryTouch.clientX, primaryTouch.clientY, this.node.getSelection())) { if (DOM.isPointInSelection(primaryTouch.clientX, primaryTouch.clientY, window.getSelection())) {
return; return;
} }
@ -224,8 +224,8 @@ class TextScanner {
} }
} }
setEnabled(enabled) { setEnabled(enabled, canEnable) {
if (enabled) { if (enabled && canEnable) {
if (!this.enabled) { if (!this.enabled) {
this.hookEvents(); this.hookEvents();
this.enabled = true; this.enabled = true;
@ -271,9 +271,9 @@ class TextScanner {
]; ];
} }
setOptions(options) { setOptions(options, canEnable=true) {
this.options = options; this.options = options;
this.setEnabled(this.options.general.enable); this.setEnabled(this.options.general.enable, canEnable);
} }
async searchAt(x, y, cause) { async searchAt(x, y, cause) {

44
package-lock.json generated
View File

@ -906,9 +906,9 @@
"dev": true "dev": true
}, },
"jsdom": { "jsdom": {
"version": "16.2.1", "version": "16.2.2",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.2.1.tgz", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.2.2.tgz",
"integrity": "sha512-3p0gHs5EfT7PxW9v8Phz3mrq//4Dy8MQenU/PoKxhdT+c45S7NjIjKbGT3Ph0nkICweE1r36+yaknXA5WfVNAg==", "integrity": "sha512-pDFQbcYtKBHxRaP55zGXCJWgFHkDAYbKcsXEK/3Icu9nKYZkutUXfLBwbD+09XDutkYSHcgfQLZ0qvpAAm9mvg==",
"dev": true, "dev": true,
"requires": { "requires": {
"abab": "^2.0.3", "abab": "^2.0.3",
@ -931,11 +931,11 @@
"tough-cookie": "^3.0.1", "tough-cookie": "^3.0.1",
"w3c-hr-time": "^1.0.2", "w3c-hr-time": "^1.0.2",
"w3c-xmlserializer": "^2.0.0", "w3c-xmlserializer": "^2.0.0",
"webidl-conversions": "^5.0.0", "webidl-conversions": "^6.0.0",
"whatwg-encoding": "^1.0.5", "whatwg-encoding": "^1.0.5",
"whatwg-mimetype": "^2.3.0", "whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0", "whatwg-url": "^8.0.0",
"ws": "^7.2.1", "ws": "^7.2.3",
"xml-name-validator": "^3.0.0" "xml-name-validator": "^3.0.0"
}, },
"dependencies": { "dependencies": {
@ -946,6 +946,14 @@
"dev": true, "dev": true,
"requires": { "requires": {
"webidl-conversions": "^5.0.0" "webidl-conversions": "^5.0.0"
},
"dependencies": {
"webidl-conversions": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
"integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
"dev": true
}
} }
}, },
"tr46": { "tr46": {
@ -958,9 +966,9 @@
} }
}, },
"webidl-conversions": { "webidl-conversions": {
"version": "5.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.0.0.tgz",
"integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", "integrity": "sha512-jTZAeJnc6D+yAOjygbJOs33kVQIk5H6fj9SFDOhIKjsf9HiAzL/c+tAJsc8ASWafvhNkH+wJZms47pmajkhatA==",
"dev": true "dev": true
}, },
"whatwg-url": { "whatwg-url": {
@ -972,6 +980,14 @@
"lodash.sortby": "^4.7.0", "lodash.sortby": "^4.7.0",
"tr46": "^2.0.0", "tr46": "^2.0.0",
"webidl-conversions": "^5.0.0" "webidl-conversions": "^5.0.0"
},
"dependencies": {
"webidl-conversions": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
"integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==",
"dev": true
}
} }
} }
} }
@ -1199,9 +1215,9 @@
"dev": true "dev": true
}, },
"psl": { "psl": {
"version": "1.7.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.7.0.tgz", "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz",
"integrity": "sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ==", "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==",
"dev": true "dev": true
}, },
"punycode": { "punycode": {
@ -1362,9 +1378,9 @@
"dev": true "dev": true
}, },
"saxes": { "saxes": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.0.tgz", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
"integrity": "sha512-LXTZygxhf8lfwKaTP/8N9CsVdjTlea3teze4lL6u37ivbgGbV0GGMuNtS/I9rnD/HC2/txUM7Df4S2LVl1qhiA==", "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==",
"dev": true, "dev": true,
"requires": { "requires": {
"xmlchars": "^2.2.0" "xmlchars": "^2.2.0"

View File

@ -30,6 +30,6 @@
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-plugin-no-unsanitized": "^3.0.2", "eslint-plugin-no-unsanitized": "^3.0.2",
"fake-indexeddb": "^3.0.0", "fake-indexeddb": "^3.0.0",
"jsdom": "^16.2.1" "jsdom": "^16.2.2"
} }
} }

View File

@ -37,6 +37,18 @@ function getNewline(string) {
} }
} }
function getSubstringCount(string, substring) {
let start = 0;
let count = 0;
while (true) {
const pos = string.indexOf(substring, start);
if (pos < 0) { break; }
++count;
start = pos + substring.length;
}
return count;
}
function validateGlobals(fileName, fix) { function validateGlobals(fileName, fix) {
const pattern = /\/\*\s*global\s+([\w\W]*?)\*\//g; const pattern = /\/\*\s*global\s+([\w\W]*?)\*\//g;
@ -47,6 +59,7 @@ function validateGlobals(fileName, fix) {
let first = true; let first = true;
let endIndex = 0; let endIndex = 0;
let newSource = ''; let newSource = '';
const allGlobals = [];
const newline = getNewline(source); const newline = getNewline(source);
while ((match = pattern.exec(source)) !== null) { while ((match = pattern.exec(source)) !== null) {
if (!first) { if (!first) {
@ -74,15 +87,27 @@ function validateGlobals(fileName, fix) {
newSource += source.substring(0, match.index); newSource += source.substring(0, match.index);
newSource += expected; newSource += expected;
endIndex = match.index + match[0].length; endIndex = match.index + match[0].length;
allGlobals.push(...parts);
} }
newSource += source.substring(endIndex); newSource += source.substring(endIndex);
// This is an approximate check to see if a global variable is unused.
// If the global appears in a comment, string, or similar, the check will pass.
let errorCount = 0;
for (const global of allGlobals) {
if (getSubstringCount(newSource, global) <= 1) {
console.error(`Global variable ${global} appears to be unused in ${fileName}`);
++errorCount;
}
}
if (fix) { if (fix) {
fs.writeFileSync(fileName, newSource, {encoding: 'utf8'}); fs.writeFileSync(fileName, newSource, {encoding: 'utf8'});
} }
return true; return errorCount === 0;
} }

View File

@ -176,19 +176,19 @@ function testConvertReading() {
[['アリガトウ', 'アリガトウ', 'hiragana'], 'ありがとう'], [['アリガトウ', 'アリガトウ', 'hiragana'], 'ありがとう'],
[['アリガトウ', 'アリガトウ', 'katakana'], 'アリガトウ'], [['アリガトウ', 'アリガトウ', 'katakana'], 'アリガトウ'],
[['アリガトウ', 'アリガトウ', 'romaji'], 'arigatou'], [['アリガトウ', 'アリガトウ', 'romaji'], 'arigatou'],
[['アリガトウ', 'アリガトウ', 'none'], null], [['アリガトウ', 'アリガトウ', 'none'], ''],
[['アリガトウ', 'アリガトウ', 'default'], 'アリガトウ'], [['アリガトウ', 'アリガトウ', 'default'], 'アリガトウ'],
[['ありがとう', 'ありがとう', 'hiragana'], 'ありがとう'], [['ありがとう', 'ありがとう', 'hiragana'], 'ありがとう'],
[['ありがとう', 'ありがとう', 'katakana'], 'アリガトウ'], [['ありがとう', 'ありがとう', 'katakana'], 'アリガトウ'],
[['ありがとう', 'ありがとう', 'romaji'], 'arigatou'], [['ありがとう', 'ありがとう', 'romaji'], 'arigatou'],
[['ありがとう', 'ありがとう', 'none'], null], [['ありがとう', 'ありがとう', 'none'], ''],
[['ありがとう', 'ありがとう', 'default'], 'ありがとう'], [['ありがとう', 'ありがとう', 'default'], 'ありがとう'],
[['有り難う', 'ありがとう', 'hiragana'], 'ありがとう'], [['有り難う', 'ありがとう', 'hiragana'], 'ありがとう'],
[['有り難う', 'ありがとう', 'katakana'], 'アリガトウ'], [['有り難う', 'ありがとう', 'katakana'], 'アリガトウ'],
[['有り難う', 'ありがとう', 'romaji'], 'arigatou'], [['有り難う', 'ありがとう', 'romaji'], 'arigatou'],
[['有り難う', 'ありがとう', 'none'], null], [['有り難う', 'ありがとう', 'none'], ''],
[['有り難う', 'ありがとう', 'default'], 'ありがとう'], [['有り難う', 'ありがとう', 'default'], 'ありがとう'],
// Cases with falsy readings // Cases with falsy readings
@ -196,44 +196,20 @@ function testConvertReading() {
[['ありがとう', '', 'hiragana'], ''], [['ありがとう', '', 'hiragana'], ''],
[['ありがとう', '', 'katakana'], ''], [['ありがとう', '', 'katakana'], ''],
[['ありがとう', '', 'romaji'], 'arigatou'], [['ありがとう', '', 'romaji'], 'arigatou'],
[['ありがとう', '', 'none'], null], [['ありがとう', '', 'none'], ''],
[['ありがとう', '', 'default'], ''], [['ありがとう', '', 'default'], ''],
[['ありがとう', null, 'hiragana'], ''],
[['ありがとう', null, 'katakana'], ''],
[['ありがとう', null, 'romaji'], 'arigatou'],
[['ありがとう', null, 'none'], null],
[['ありがとう', null, 'default'], null],
[['ありがとう', void 0, 'hiragana'], ''],
[['ありがとう', void 0, 'katakana'], ''],
[['ありがとう', void 0, 'romaji'], 'arigatou'],
[['ありがとう', void 0, 'none'], null],
[['ありがとう', void 0, 'default'], void 0],
// Cases with falsy readings and kanji expressions // Cases with falsy readings and kanji expressions
[['有り難う', '', 'hiragana'], ''], [['有り難う', '', 'hiragana'], ''],
[['有り難う', '', 'katakana'], ''], [['有り難う', '', 'katakana'], ''],
[['有り難う', '', 'romaji'], ''], [['有り難う', '', 'romaji'], ''],
[['有り難う', '', 'none'], null], [['有り難う', '', 'none'], ''],
[['有り難う', '', 'default'], ''], [['有り難う', '', 'default'], '']
[['有り難う', null, 'hiragana'], ''],
[['有り難う', null, 'katakana'], ''],
[['有り難う', null, 'romaji'], null],
[['有り難う', null, 'none'], null],
[['有り難う', null, 'default'], null],
[['有り難う', void 0, 'hiragana'], ''],
[['有り難う', void 0, 'katakana'], ''],
[['有り難う', void 0, 'romaji'], void 0],
[['有り難う', void 0, 'none'], null],
[['有り難う', void 0, 'default'], void 0]
]; ];
for (const [[expressionFragment, readingFragment, readingMode], expected] of data) { for (const [[expression, reading, readingMode], expected] of data) {
assert.strictEqual(jp.convertReading(expressionFragment, readingFragment, readingMode), expected); assert.strictEqual(jp.convertReading(expression, reading, readingMode), expected);
} }
} }
@ -303,9 +279,9 @@ function testDistributeFurigana() {
['有り難う', 'ありがとう'], ['有り難う', 'ありがとう'],
[ [
{text: '有', furigana: 'あ'}, {text: '有', furigana: 'あ'},
{text: 'り'}, {text: 'り', furigana: ''},
{text: '難', furigana: 'がと'}, {text: '難', furigana: 'がと'},
{text: 'う'} {text: 'う', furigana: ''}
] ]
], ],
[ [
@ -317,23 +293,23 @@ function testDistributeFurigana() {
[ [
['お祝い', 'おいわい'], ['お祝い', 'おいわい'],
[ [
{text: 'お'}, {text: 'お', furigana: ''},
{text: '祝', furigana: 'いわ'}, {text: '祝', furigana: 'いわ'},
{text: 'い'} {text: 'い', furigana: ''}
] ]
], ],
[ [
['美味しい', 'おいしい'], ['美味しい', 'おいしい'],
[ [
{text: '美味', furigana: 'おい'}, {text: '美味', furigana: 'おい'},
{text: 'しい'} {text: 'しい', furigana: ''}
] ]
], ],
[ [
['食べ物', 'たべもの'], ['食べ物', 'たべもの'],
[ [
{text: '食', furigana: 'た'}, {text: '食', furigana: 'た'},
{text: 'べ'}, {text: 'べ', furigana: ''},
{text: '物', furigana: 'もの'} {text: '物', furigana: 'もの'}
] ]
], ],
@ -341,9 +317,9 @@ function testDistributeFurigana() {
['試し切り', 'ためしぎり'], ['試し切り', 'ためしぎり'],
[ [
{text: '試', furigana: 'ため'}, {text: '試', furigana: 'ため'},
{text: 'し'}, {text: 'し', furigana: ''},
{text: '切', furigana: 'ぎ'}, {text: '切', furigana: 'ぎ'},
{text: 'り'} {text: 'り', furigana: ''}
] ]
], ],
// Ambiguous // Ambiguous
@ -373,16 +349,16 @@ function testDistributeFuriganaInflected() {
['美味しい', 'おいしい', '美味しかた'], ['美味しい', 'おいしい', '美味しかた'],
[ [
{text: '美味', furigana: 'おい'}, {text: '美味', furigana: 'おい'},
{text: 'し'}, {text: 'し', furigana: ''},
{text: 'かた'} {text: 'かた', furigana: ''}
] ]
], ],
[ [
['食べる', 'たべる', '食べた'], ['食べる', 'たべる', '食べた'],
[ [
{text: '食', furigana: 'た'}, {text: '食', furigana: 'た'},
{text: 'べ'}, {text: 'べ', furigana: ''},
{text: 'た'} {text: 'た', furigana: ''}
] ]
] ]
]; ];
@ -393,6 +369,59 @@ function testDistributeFuriganaInflected() {
} }
} }
function testCollapseEmphaticSequences() {
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 TextSourceMap(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 TextSourceMap(text, expectedSourceMapping)));
}
}
}
function testIsMoraPitchHigh() { function testIsMoraPitchHigh() {
const data = [ const data = [
[[0, 0], false], [[0, 0], false],
@ -462,6 +491,7 @@ function main() {
testConvertAlphabeticToKana(); testConvertAlphabeticToKana();
testDistributeFurigana(); testDistributeFurigana();
testDistributeFuriganaInflected(); testDistributeFuriganaInflected();
testCollapseEmphaticSequences();
testIsMoraPitchHigh(); testIsMoraPitchHigh();
testGetKanaMorae(); testGetKanaMorae();
} }