@@ -34,6 +34,7 @@ import {OptionsUtil} from '../data/options-util.js';
3434import { getAllPermissions , hasPermissions , hasRequiredPermissionsForOptions } from '../data/permissions-util.js' ;
3535import { DictionaryDatabase } from '../dictionary/dictionary-database.js' ;
3636import { Environment } from '../extension/environment.js' ;
37+ import { CacheMap } from '../general/cache-map.js' ;
3738import { ObjectPropertyAccessor } from '../general/object-property-accessor.js' ;
3839import { distributeFuriganaInflected , isCodePointJapanese , convertKatakanaToHiragana as jpConvertKatakanaToHiragana } from '../language/ja/japanese.js' ;
3940import { getLanguageSummaries , isTextLookupWorthy } from '../language/languages.js' ;
@@ -208,6 +209,8 @@ export class Backend {
208209
209210 /** @type {RikaitanApi } */
210211 this . _rikaitanApi = new RikaitanApi ( this . _apiMap , this . _offscreen ) ;
212+ /** @type {CacheMap<string, {originalTextLength: number, textSegments: import('api').ParseTextSegment[]}> } */
213+ this . _textParseCache = new CacheMap ( 10000 , 3600000 ) ; // 1 hour idle time, ~32MB per 1000 entries for Japanese
211214 }
212215
213216 /**
@@ -558,33 +561,46 @@ export class Backend {
558561
559562 /** @type {import('api').ApiHandler<'parseText'> } */
560563 async _onApiParseText ( { text, optionsContext, scanLength, useInternalParser, useMecabParser} ) {
561- const [ internalResults , mecabResults ] = await Promise . all ( [
562- ( useInternalParser ? this . _textParseScanning ( text , scanLength , optionsContext ) : null ) ,
563- ( useMecabParser ? this . _textParseMecab ( text ) : null ) ,
564- ] ) ;
565-
566564 /** @type {import('api').ParseTextResultItem[] } */
567565 const results = [ ] ;
568566
569- if ( internalResults !== null ) {
570- results . push ( {
571- id : 'scan' ,
572- source : 'scanning-parser' ,
573- dictionary : null ,
574- content : internalResults ,
575- } ) ;
576- }
567+ const [ internalResults , mecabResults ] = await Promise . all ( [
568+ useInternalParser ?
569+ ( Array . isArray ( text ) ?
570+ Promise . all ( text . map ( ( t ) => this . _textParseScanning ( t , scanLength , optionsContext ) ) ) :
571+ Promise . all ( [ this . _textParseScanning ( text , scanLength , optionsContext ) ] ) ) :
572+ null ,
573+ useMecabParser ?
574+ ( Array . isArray ( text ) ?
575+ Promise . all ( text . map ( ( t ) => this . _textParseMecab ( t ) ) ) :
576+ Promise . all ( [ this . _textParseMecab ( text ) ] ) ) :
577+ null ,
578+ ] ) ;
577579
578- if ( mecabResults !== null ) {
579- for ( const [ dictionary , content ] of mecabResults ) {
580+ if ( internalResults !== null ) {
581+ for ( const [ index , internalResult ] of internalResults . entries ( ) ) {
580582 results . push ( {
581- id : `mecab-${ dictionary } ` ,
582- source : 'mecab' ,
583- dictionary,
584- content,
583+ id : 'scan' ,
584+ source : 'scanning-parser' ,
585+ dictionary : null ,
586+ index,
587+ content : internalResult ,
585588 } ) ;
586589 }
587590 }
591+ if ( mecabResults !== null ) {
592+ for ( const [ index , mecabResult ] of mecabResults . entries ( ) ) {
593+ for ( const [ dictionary , content ] of mecabResult ) {
594+ results . push ( {
595+ id : `mecab-${ dictionary } ` ,
596+ source : 'mecab' ,
597+ dictionary,
598+ index,
599+ content,
600+ } ) ;
601+ }
602+ }
603+ }
588604
589605 return results ;
590606 }
@@ -1496,6 +1512,8 @@ export class Backend {
14961512
14971513 void this . _accessibilityController . update ( this . _getOptionsFull ( false ) ) ;
14981514
1515+ this . _textParseCache . clear ( ) ;
1516+
14991517 this . _sendMessageAllTabsIgnoreResponse ( { action : 'applicationOptionsUpdated' , params : { source} } ) ;
15001518 }
15011519
@@ -1677,25 +1695,54 @@ export class Backend {
16771695 let i = 0 ;
16781696 const ii = text . length ;
16791697 while ( i < ii ) {
1680- const { dictionaryEntries, originalTextLength} = await this . _translator . findTerms (
1681- mode ,
1682- text . substring ( i , i + scanLength ) ,
1683- findTermsOptions ,
1684- ) ;
16851698 const codePoint = /** @type {number } */ ( text . codePointAt ( i ) ) ;
16861699 const character = String . fromCodePoint ( codePoint ) ;
1687- if (
1688- dictionaryEntries . length > 0 &&
1700+ const substring = text . substring ( i , i + scanLength ) ;
1701+ const cacheKey = `${ optionsContext . index } :${ substring } ` ;
1702+ let cached = this . _textParseCache . get ( cacheKey ) ;
1703+ if ( typeof cached === 'undefined' ) {
1704+ const { dictionaryEntries, originalTextLength} = await this . _translator . findTerms (
1705+ mode ,
1706+ substring ,
1707+ findTermsOptions ,
1708+ ) ;
1709+ /** @type {import('api').ParseTextSegment[] } */
1710+ const textSegments = [ ] ;
1711+ if ( dictionaryEntries . length > 0 &&
16891712 originalTextLength > 0 &&
16901713 ( originalTextLength !== character . length || isCodePointJapanese ( codePoint ) )
1691- ) {
1692- previousUngroupedSegment = null ;
1693- const { headwords : [ { term, reading} ] } = dictionaryEntries [ 0 ] ;
1694- const source = text . substring ( i , i + originalTextLength ) ;
1695- const textSegments = [ ] ;
1696- for ( const { text : text2 , reading : reading2 } of distributeFuriganaInflected ( term , reading , source ) ) {
1697- textSegments . push ( { text : text2 , reading : reading2 } ) ;
1714+ ) {
1715+ const { headwords : [ { term, reading} ] } = dictionaryEntries [ 0 ] ;
1716+ const source = substring . substring ( 0 , originalTextLength ) ;
1717+ for ( const { text : text2 , reading : reading2 } of distributeFuriganaInflected ( term , reading , source ) ) {
1718+ textSegments . push ( { text : text2 , reading : reading2 } ) ;
1719+ }
1720+ if ( textSegments . length > 0 ) {
1721+ const token = textSegments . map ( ( s ) => s . text ) . join ( '' ) ;
1722+ const trimmedHeadwords = [ ] ;
1723+ for ( const dictionaryEntry of dictionaryEntries ) {
1724+ const validHeadwords = [ ] ;
1725+ for ( const headword of dictionaryEntry . headwords ) {
1726+ const validSources = [ ] ;
1727+ for ( const src of headword . sources ) {
1728+ if ( src . originalText !== token ) { continue ; }
1729+ if ( ! src . isPrimary ) { continue ; }
1730+ if ( src . matchType !== 'exact' ) { continue ; }
1731+ validSources . push ( src ) ;
1732+ }
1733+ if ( validSources . length > 0 ) { validHeadwords . push ( { term : headword . term , reading : headword . reading , sources : validSources } ) ; }
1734+ }
1735+ if ( validHeadwords . length > 0 ) { trimmedHeadwords . push ( validHeadwords ) ; }
1736+ }
1737+ textSegments [ 0 ] . headwords = trimmedHeadwords ;
1738+ }
16981739 }
1740+ cached = { originalTextLength, textSegments} ;
1741+ if ( typeof optionsContext . index !== 'undefined' ) { this . _textParseCache . set ( cacheKey , cached ) ; }
1742+ }
1743+ const { originalTextLength, textSegments} = cached ;
1744+ if ( textSegments . length > 0 ) {
1745+ previousUngroupedSegment = null ;
16991746 results . push ( textSegments ) ;
17001747 i += originalTextLength ;
17011748 } else {
0 commit comments