示例#1
0
    def save(self):
        """
        Save all of the program settings to disk.
            They must have already been saved into self.data.
        """
        if BibleOrgSysGlobals.debugFlag and debuggingThisModule:
            print(
                exp("ApplicationSettings.save() in {!r}").format(
                    self.settingsFilepath))
            assert self.data
            assert self.settingsFilepath

        BibleOrgSysGlobals.backupAnyExistingFile(self.settingsFilepath,
                                                 numBackups=4)
        with open(
                self.settingsFilepath, 'wt', encoding='utf-8'
        ) as settingsFile:  # It may or may not have previously existed
            # Put a (comment) heading in the file first
            settingsFile.write(
                '# ' +
                _("{} {} settings file").format(APP_NAME, SettingsVersion) +
                '\n')
            settingsFile.write( '# ' + _("Originally saved {} as {}") \
                .format( datetime.now().strftime('%Y-%m-%d %H:%M:%S'), self.settingsFilepath ) + '\n\n' )

            self.data.write(settingsFile)
示例#2
0
    def saveAnyChangedGlosses( self, exportAlso=False ):
        """
        Save the glossing dictionary to a pickle file.
        """
        if debuggingThisModule: print( "saveAnyChangedGlosses()" )

        if self.haveGlossingDictChanges:
            BibleOrgSysGlobals.backupAnyExistingFile( self.glossingDictFilepath, 9 )
            if BibleOrgSysGlobals.verbosityLevel > 2 or debuggingThisModule:
                print( "  Saving Hebrew glossing dictionary ({}->{} entries) to '{}'…".format( self.loadedGlossEntryCount, len(self.glossingDict), self.glossingDictFilepath ) )
            elif BibleOrgSysGlobals.verbosityLevel > 1:
                print( "  Saving Hebrew glossing dictionary ({}->{} entries)…".format( self.loadedGlossEntryCount, len(self.glossingDict) ) )
            with open( self.glossingDictFilepath, 'wb' ) as pickleFile:
                pickle.dump( self.glossingDict, pickleFile )

            if exportAlso: self.exportGlossingDictionary()
示例#3
0
    def save( self ):
        """
        Save all of the program settings to disk.
            They must have already been saved into self.data.
        """
        if BibleOrgSysGlobals.debugFlag and debuggingThisModule:
            print( exp("ApplicationSettings.save() in {!r}").format( self.settingsFilepath ) )
            assert self.data
            assert self.settingsFilepath

        BibleOrgSysGlobals.backupAnyExistingFile( self.settingsFilepath, numBackups=8 )
        with open( self.settingsFilepath, 'wt', encoding='utf-8' ) as settingsFile: # It may or may not have previously existed
            # Put a (comment) heading in the file first
            settingsFile.write( '# ' + _("{} {} settings file").format( APP_NAME, SettingsVersion ) + '\n' )
            settingsFile.write( '# ' + _("Originally saved {} as {}") \
                .format( datetime.now().strftime('%Y-%m-%d %H:%M:%S'), self.settingsFilepath ) + '\n\n' )

            self.data.write( settingsFile )
示例#4
0
    def exportGlossingDictionary( self, glossingDictExportFilepath=None ):
        """
        Export the glossing dictionary to a text file
            plus a reversed text file (without the references).

        Also does a few checks while exporting.
            (These can be fixed and then the file can be imported.)
        """
        #print( "exportGlossingDictionary()" )
        if glossingDictExportFilepath is None: glossingDictExportFilepath = DEFAULT_GLOSSING_EXPORT_FILEPATH
        if BibleOrgSysGlobals.verbosityLevel > 1:
            print( _("Exporting glossing dictionary ({} entries) to '{}'…").format( len(self.glossingDict), glossingDictExportFilepath ) )

        BibleOrgSysGlobals.backupAnyExistingFile( glossingDictExportFilepath, 5 )
        with open( glossingDictExportFilepath, 'wt' ) as exportFile:
            for word,(genericGloss,genericReferencesList,specificReferencesDict) in self.glossingDict.items():
                if ' ' in word or '/' in word:
                    logging.error( _("Word {!r} has illegal characters").format( word ) )
                if ' ' in genericGloss:
                    logging.error( _("Generic gloss {!r} for {!r} has illegal characters").format( genericGloss, word ) )
                if word.count('=') != genericGloss.count('='):
                    logging.error( _("Generic gloss {!r} and word {!r} has different numbers of morphemes").format( genericGloss, word ) )
                if not genericReferencesList:
                    logging.error( _("Generic gloss {!r} for {!r} has no references").format( genericGloss, word ) )
                exportFile.write( '{}  {}  {}  {}\n'.format( genericReferencesList, specificReferencesDict, genericGloss, word ) ) # Works best in editors with English on the left, Hebrew on the right

        if self.glossingDict:
            if BibleOrgSysGlobals.verbosityLevel > 1:
                print( _("Exporting reverse glossing dictionary ({} entries) to '{}'…").format( len(self.glossingDict), DEFAULT_GENERIC_GLOSSING_REVERSE_EXPORT_FILEPATH ) )
            BibleOrgSysGlobals.backupAnyExistingFile( DEFAULT_GENERIC_GLOSSING_REVERSE_EXPORT_FILEPATH, 5 )
            doneGlosses = []
            with open( DEFAULT_GENERIC_GLOSSING_REVERSE_EXPORT_FILEPATH, 'wt' ) as exportFile:
                for word,(genericGloss,genericReferencesList,specificReferencesDict) in sorted( self.glossingDict.items(), key=lambda theTuple: theTuple[1][0].lower() ):
                    if genericGloss in doneGlosses:
                        logging.warning( _("Generic gloss {!r} has already appeared: currently for word {!r}").format( genericGloss, word ) )
                    exportFile.write( '{}  {}\n'.format( genericGloss, word ) ) # Works best in editors with English on the left, Hebrew on the right
                    doneGlosses.append( genericGloss )
示例#5
0
def searchReplaceText( self, optionsDict, confirmCallback ):
    """
    Search the Bible book files for the given text which is contained in a dictionary of options.
        Search string must be in optionsDict['searchText'].
        (We add default options for any missing ones as well as updating the 'searchHistoryList'.)
    Then go through and replace.

    "self" in this case is either a USFMBible or a PTXBible object.

    The confirmCallback function must be a function that takes
        6 parameters: ref, contextBefore, ourSearchText, contextAfter, willBeText, haveUndosFlag
    and returns a single UPPERCASE character
        'N' (no), 'Y' (yes), 'A' (all), or 'S' (stop).

    Note that this function works on actual text files.
        If the text files are loaded into a Bible object,
            after replacing, the Bible object will need to be reloaded.
    If it's called from an edit window, it's essential that all editing changes
        are saved to the file first.

    NOTE: We currently handle undo, by cache all files which need to be saved to disk.
        We might need to make this more efficient, e.g., save under a temp filename.
    """
    if BibleOrgSysGlobals.debugFlag:
        if debuggingThisModule:
            print( exp("searchReplaceText( {}, {}, … )").format( self, optionsDict ) )
            assert 'searchText' in optionsDict
            assert 'replaceText' in optionsDict

    optionsList = ( 'searchText', 'replaceText', 'work', 'searchHistoryList', 'replaceHistoryList', 'wordMode',
            #'caselessFlag', 'ignoreDiacriticsFlag', 'includeIntroFlag', 'includeMainTextFlag',
            #'includeMarkerTextFlag', 'includeExtrasFlag', 'markerList', 'chapterList',
            'contextLength', 'bookList', 'regexFlag', 'currentBCV', 'doBackups', )
    for someKey in optionsDict:
        if someKey not in optionsList:
            print( "searchReplaceText warning: unexpected {!r} option = {!r}".format( someKey, optionsDict[someKey] ) )
            if debuggingThisModule: halt

    # Go through all the given options
    if 'work' not in optionsDict: optionsDict['work'] = self.abbreviation if self.abbreviation else self.name
    if 'searchHistoryList' not in optionsDict: optionsDict['searchHistoryList'] = [] # Oldest first
    if 'wordMode' not in optionsDict: optionsDict['wordMode'] = 'Any' # or 'Whole' or 'EndsWord' or 'Begins' or 'EndsLine'
    #if 'caselessFlag' not in optionsDict: optionsDict['caselessFlag'] = True
    #if 'ignoreDiacriticsFlag' not in optionsDict: optionsDict['ignoreDiacriticsFlag'] = False
    #if 'includeIntroFlag' not in optionsDict: optionsDict['includeIntroFlag'] = True
    #if 'includeMainTextFlag' not in optionsDict: optionsDict['includeMainTextFlag'] = True
    #if 'includeMarkerTextFlag' not in optionsDict: optionsDict['includeMarkerTextFlag'] = False
    #if 'includeExtrasFlag' not in optionsDict: optionsDict['includeExtrasFlag'] = False
    if 'contextLength' not in optionsDict: optionsDict['contextLength'] = 60 # each side
    if 'bookList' not in optionsDict: optionsDict['bookList'] = 'ALL' # or BBB or a list
    #if 'chapterList' not in optionsDict: optionsDict['chapterList'] = None
    #if 'markerList' not in optionsDict: optionsDict['markerList'] = None
    if 'doBackups' not in optionsDict: optionsDict['doBackups'] = True
    optionsDict['regexFlag'] = False

    if BibleOrgSysGlobals.debugFlag:
        if optionsDict['chapterList']: assert optionsDict['bookList'] is None or len(optionsDict['bookList']) == 1 \
                            or optionsDict['chapterList'] == [0] # Only combinations that make sense
        assert '\r' not in optionsDict['searchText'] and '\n' not in optionsDict['searchText']
        assert optionsDict['wordMode'] in ( 'Any', 'Whole', 'Begins', 'EndsWord', 'EndsLine', )
        if optionsDict['wordMode'] != 'Any': assert ' ' not in optionsDict['searchText']
        #if optionsDict['markerList']:
            #assert isinstance( markerList, list )
            #assert not optionsDict['includeIntroFlag']
            #assert not optionsDict['includeMainTextFlag']
            #assert not optionsDict['includeMarkerTextFlag']
            #assert not optionsDict['includeExtrasFlag']

    #ourMarkerList = []
    #if optionsDict['markerList']:
        #for marker in optionsDict['markerList']:
            #ourMarkerList.append( BibleOrgSysGlobals.USFMMarkers.toStandardMarker( marker ) )

    resultDict = { 'numFinds':0, 'numReplaces':0, 'searchedBookList':[], 'foundBookList':[], 'replacedBookList':[], 'aborted':False, }

    ourSearchText = optionsDict['searchText']
    # Save the search history (with the 'regex:' text still prefixed if applicable)
    try: optionsDict['searchHistoryList'].remove( ourSearchText )
    except ValueError: pass
    optionsDict['searchHistoryList'].append( ourSearchText ) # Make sure it goes on the end

    ourReplaceText = optionsDict['replaceText']
    try: optionsDict['replaceHistoryList'].remove( ourReplaceText )
    except ValueError: pass
    optionsDict['replaceHistoryList'].append( ourReplaceText ) # Make sure it goes on the end

    if ourSearchText.lower().startswith( 'regex:' ):
        resultDict['hadRegexError'] = False
        optionsDict['regexFlag'] = True
        ourSearchText = ourSearchText[6:]
        compiledSearchText = re.compile( ourSearchText )
    else:
        replaceLen = len( ourReplaceText )
        #diffLen = replaceLen - searchLen
    #if optionsDict['ignoreDiacriticsFlag']: ourSearchText = BibleOrgSysGlobals.removeAccents( ourSearchText )
    #if optionsDict['caselessFlag']: ourSearchText = ourSearchText.lower()
    searchLen = len( ourSearchText )
    if BibleOrgSysGlobals.debugFlag: assert searchLen
    #print( "  Searching for {!r} in {} loaded books".format( ourSearchText, len(self) ) )

    if not self.preloadDone: self.preload()

    # The first entry in the result list is a dictionary containing the parameters
    #   Following entries are SimpleVerseKey objects
    encoding = self.encoding
    if encoding is None: encoding = 'utf-8'

    replaceAllFlag = stopFlag = undoFlag = False
    filesToSave = OrderedDict()
    if self.maximumPossibleFilenameTuples:
        for BBB,filename in self.maximumPossibleFilenameTuples:
            if optionsDict['bookList'] is None or optionsDict['bookList']=='ALL' or BBB in optionsDict['bookList']:
                #print( exp("searchReplaceText: will search book {}").format( BBB ) )
                bookFilepath = os.path.join( self.sourceFolder, filename )
                with open( bookFilepath, 'rt', encoding=encoding ) as bookFile:
                    bookText = bookFile.read()
                resultDict['searchedBookList'].append( BBB )

                #C = V = '0'
                if optionsDict['regexFlag']: # ignores wordMode flag
                    ix = 0
                    while True:
                        match = compiledSearchText.search( bookText, ix )
                        if not match: break # none / no more found
                        ix, ixAfter = match.span()
                        regexFoundText = bookText[ix:ixAfter]
                        try: regexReplacementText = compiledSearchText.sub( ourReplaceText, regexFoundText, count=1 )
                        except re.error as err:
                            print( "Search/Replace regex error: {}".format( err ) )
                            resultDict['hadRegexError'] = True
                            stopFlag = True; break
                        #print( "Found regex {!r} at {:,} in {}".format( ourSearchText, ix, BBB ) )
                        #print( "  Found text was {!r}, replacement will be {!r}".format( regexFoundText, regexReplacementText ) )
                        resultDict['numFinds'] += 1
                        if BBB not in resultDict['foundBookList']: resultDict['foundBookList'].append( BBB )

                        if optionsDict['contextLength']: # Find the context in the original (fully-cased) string
                            contextBefore = bookText[max(0,ix-optionsDict['contextLength']):ix]
                            contextAfter = bookText[ixAfter:ixAfter+optionsDict['contextLength']]
                        else: contextBefore = contextAfter = None
                        #print( "  After  {!r}".format( contextBefore ) )
                        #print( "  Before {!r}".format( contextAfter ) )

                        result = None
                        if not replaceAllFlag:
                            ref = BBB
                            willBeText = contextBefore + regexReplacementText + contextAfter
                            result = confirmCallback( ref, contextBefore, regexFoundText, contextAfter, willBeText, resultDict['numReplaces']>0 )
                            #print( "searchReplaceText got", result )
                            assert result in 'YNASU'
                            if result == 'A': replaceAllFlag = True
                            elif result == 'S': stopFlag = True; break
                            elif result == 'U': undoFlag = True; break
                        if replaceAllFlag or result == 'Y':
                            #print( "  ix={:,}, ixAfter={:,}, diffLen={}".format( ix, ixAfter, diffLen ) )
                            bookText = bookText[:ix] + regexReplacementText + bookText[ixAfter:]
                            ix += len( regexReplacementText ) # Start searching after the replacement
                            #print( "  ix={:,}, ixAfter={:,}, now={!r}".format( ix, ixAfter, bookText ) )
                            resultDict['numReplaces'] += 1
                            if BBB not in resultDict['replacedBookList']: resultDict['replacedBookList'].append( BBB )
                            filesToSave[BBB] = (bookFilepath,bookText)
                        else: ix += 1 # So don't keep repeating the same find
                else: # not regExp
                    ix = 0
                    while True:
                        ix = bookText.find( ourSearchText, ix )
                        if ix == -1: break # none / no more found
                        #print( "Found {!r} at {:,} in {}".format( ourSearchText, ix, BBB ) )
                        resultDict['numFinds'] += 1
                        if BBB not in resultDict['foundBookList']: resultDict['foundBookList'].append( BBB )

                        ixAfter = ix + searchLen
                        if optionsDict['wordMode'] == 'Whole':
                            #print( "BF", repr(bookText[ix-1]) )
                            #print( "AF", repr(bookText[ixAfter]) )
                            if ix>0 and bookText[ix-1].isalpha(): ix+=1; continue
                            if ixAfter<len(bookText) and bookText[ixAfter].isalpha(): ix+=1; continue
                        elif optionsDict['wordMode'] == 'Begins':
                            if ix>0 and bookText[ix-1].isalpha(): ix+=1; continue
                        elif optionsDict['wordMode'] == 'EndsWord':
                            if ixAfter<len(bookText) and bookText[ixAfter].isalpha(): ix+=1; continue
                        elif optionsDict['wordMode'] == 'EndsLine':
                            if ixAfter<len(bookText): ix+=1; continue

                        if optionsDict['contextLength']: # Find the context in the original (fully-cased) string
                            contextBefore = bookText[max(0,ix-optionsDict['contextLength']):ix]
                            contextAfter = bookText[ixAfter:ixAfter+optionsDict['contextLength']]
                        else: contextBefore = contextAfter = None
                        #print( "  After  {!r}".format( contextBefore ) )
                        #print( "  Before {!r}".format( contextAfter ) )

                        result = None
                        if not replaceAllFlag:
                            ref = BBB
                            willBeText = contextBefore + ourReplaceText + contextAfter
                            result = confirmCallback( ref, contextBefore, ourSearchText, contextAfter, willBeText, resultDict['numReplaces']>0 )
                            #print( "searchReplaceText got", result )
                            assert result in 'YNASU'
                            if result == 'A': replaceAllFlag = True
                            elif result == 'S': stopFlag = True; break
                            elif result == 'U': undoFlag = True; break
                        if replaceAllFlag or result == 'Y':
                            #print( "  ix={:,}, ixAfter={:,}, diffLen={}".format( ix, ixAfter, diffLen ) )
                            bookText = bookText[:ix] + ourReplaceText + bookText[ixAfter:]
                            ix += replaceLen # Start searching after the replacement
                            #print( "  ix={:,}, ixAfter={:,}, now={!r}".format( ix, ixAfter, bookText ) )
                            resultDict['numReplaces'] += 1
                            if BBB not in resultDict['replacedBookList']: resultDict['replacedBookList'].append( BBB )
                            filesToSave[BBB] = (bookFilepath,bookText)
                        else: ix += 1 # So don't keep repeating the same find

            if stopFlag:
                if BibleOrgSysGlobals.verbosityLevel > 2:
                    print( "Search/Replace was aborted in {} after {} replaces.".format( BBB, resultDict['numReplaces'] ) )
                resultDict['aborted'] = True
                break
            if undoFlag:
                if resultDict['numReplaces']>0:
                    if BibleOrgSysGlobals.verbosityLevel > 2:
                        print( "Search/Replace was aborted in {} for undo in {} books.".format( BBB, len(resultDict['replacedBookList']) ) )
                elif BibleOrgSysGlobals.verbosityLevel > 2:
                    print( "Search/Replace was aborted (by undo) in {}.".format( BBB ) )
                filesToSave = {}
                resultDict['replacedBookList'] = []
                resultDict['numReplaces'] = 0
                resultDict['aborted'] = True
                break

    else:
        logging.critical( exp("No book files to search/replace in {}!").format( self.sourceFolder ) )

    for BBB,(filepath,fileText) in filesToSave.items():
        if optionsDict['doBackups']:
            if BibleOrgSysGlobals.verbosityLevel > 2:
                print( "Making backup copy of {} file: {}…".format( BBB, filepath ) )
            BibleOrgSysGlobals.backupAnyExistingFile( filepath, numBackups=4 )
        if BibleOrgSysGlobals.verbosityLevel > 2:
            print( "Writing {:,} bytes for {} to {}…".format( len(fileText), BBB, filepath ) )
        elif BibleOrgSysGlobals.verbosityLevel > 1:
            print( "Saving {} with {} encoding".format( filepath, encoding ) )
        with open( filepath, 'wt', encoding=encoding, newline='\r\n' ) as bookFile:
            bookFile.write( fileText )
        self.bookNeedsReloading[BBB] = True

    #print( exp("searchReplaceText: returning {}/{}  {}/{}/{} books  {}").format( resultDict['numReplaces'], resultDict['numFinds'], len(resultDict['replacedBookList']), len(resultDict['foundBookList']), len(resultDict['searchedBookList']), optionsDict ) )
    return optionsDict, resultDict
示例#6
0
def findReplaceText(self, optionsDict, confirmCallback):
    """
    Search the Bible book files for the given text which is contained in a dictionary of options.
        Find string must be in optionsDict['findText'].
        (We add default options for any missing ones as well as updating the 'findHistoryList'.)
    Then go through and replace.

    "self" in this case is either a USFMBible or a PTXBible object.

    The confirmCallback function must be a function that takes
        6 parameters: ref, contextBefore, ourFindText, contextAfter, willBeText, haveUndosFlag
    and returns a single UPPERCASE character
        'N' (no), 'Y' (yes), 'A' (all), or 'S' (stop).

    Note that this function works on actual text files.
        If the text files are loaded into a Bible object,
            after replacing, the Bible object will need to be reloaded.
    If it's called from an edit window, it's essential that all editing changes
        are saved to the file first.

    NOTE: We currently handle undo, by caching all files which need to be saved to disk.
        We might need to make this more efficient, e.g., save under a temp filename.
    """
    if BibleOrgSysGlobals.debugFlag:
        if debuggingThisModule:
            print(
                exp("findReplaceText( {}, {}, … )").format(self, optionsDict))
            assert 'findText' in optionsDict
            assert 'replaceText' in optionsDict

    optionsList = (
        'parentApp',
        'parentWindow',
        'parentBox',
        'givenBible',
        'workName',
        'findText',
        'replaceText',
        'findHistoryList',
        'replaceHistoryList',
        'wordMode',
        #'caselessFlag', 'ignoreDiacriticsFlag', 'includeIntroFlag', 'includeMainTextFlag',
        #'includeMarkerTextFlag', 'includeExtrasFlag', 'markerList', 'chapterList',
        'contextLength',
        'bookList',
        'regexFlag',
        'currentBCV',
        'doBackups',
    )
    for someKey in optionsDict:
        if someKey not in optionsList:
            print("findReplaceText warning: unexpected {!r} option = {!r}".
                  format(someKey, optionsDict[someKey]))
            if debuggingThisModule: halt

    # Go through all the given options
    if 'workName' not in optionsDict:
        optionsDict[
            'workName'] = self.abbreviation if self.abbreviation else self.name
    if 'findHistoryList' not in optionsDict:
        optionsDict['findHistoryList'] = []  # Oldest first
    if 'wordMode' not in optionsDict:
        optionsDict[
            'wordMode'] = 'Any'  # or 'Whole' or 'EndsWord' or 'Begins' or 'EndsLine'
    #if 'caselessFlag' not in optionsDict: optionsDict['caselessFlag'] = True
    #if 'ignoreDiacriticsFlag' not in optionsDict: optionsDict['ignoreDiacriticsFlag'] = False
    #if 'includeIntroFlag' not in optionsDict: optionsDict['includeIntroFlag'] = True
    #if 'includeMainTextFlag' not in optionsDict: optionsDict['includeMainTextFlag'] = True
    #if 'includeMarkerTextFlag' not in optionsDict: optionsDict['includeMarkerTextFlag'] = False
    #if 'includeExtrasFlag' not in optionsDict: optionsDict['includeExtrasFlag'] = False
    if 'contextLength' not in optionsDict:
        optionsDict['contextLength'] = 60  # each side
    if 'bookList' not in optionsDict:
        optionsDict['bookList'] = 'ALL'  # or BBB or a list
    #if 'chapterList' not in optionsDict: optionsDict['chapterList'] = None
    #if 'markerList' not in optionsDict: optionsDict['markerList'] = None
    if 'doBackups' not in optionsDict: optionsDict['doBackups'] = True
    optionsDict['regexFlag'] = False

    if BibleOrgSysGlobals.debugFlag:
        if optionsDict['chapterList']:            assert optionsDict['bookList'] is None or len(optionsDict['bookList']) == 1 \
 or optionsDict['chapterList'] == [0] # Only combinations that make sense
        assert '\r' not in optionsDict['findText'] and '\n' not in optionsDict[
            'findText']
        assert optionsDict['wordMode'] in (
            'Any',
            'Whole',
            'Begins',
            'EndsWord',
            'EndsLine',
        )
        if optionsDict['wordMode'] != 'Any':
            assert ' ' not in optionsDict['findText']
        #if optionsDict['markerList']:
        #assert isinstance( markerList, list )
        #assert not optionsDict['includeIntroFlag']
        #assert not optionsDict['includeMainTextFlag']
        #assert not optionsDict['includeMarkerTextFlag']
        #assert not optionsDict['includeExtrasFlag']

    #ourMarkerList = []
    #if optionsDict['markerList']:
    #for marker in optionsDict['markerList']:
    #ourMarkerList.append( BibleOrgSysGlobals.USFMMarkers.toStandardMarker( marker ) )

    resultDict = {
        'numFinds': 0,
        'numReplaces': 0,
        'searchedBookList': [],
        'foundBookList': [],
        'replacedBookList': [],
        'aborted': False,
    }

    ourFindText = optionsDict['findText']
    # Save the search history (with the 'regex:' text still prefixed if applicable)
    try:
        optionsDict['findHistoryList'].remove(ourFindText)
    except ValueError:
        pass
    optionsDict['findHistoryList'].append(
        ourFindText)  # Make sure it goes on the end

    ourReplaceText = optionsDict['replaceText']
    try:
        optionsDict['replaceHistoryList'].remove(ourReplaceText)
    except ValueError:
        pass
    optionsDict['replaceHistoryList'].append(
        ourReplaceText)  # Make sure it goes on the end

    if ourFindText.lower().startswith('regex:'):
        resultDict['hadRegexError'] = False
        optionsDict['regexFlag'] = True
        ourFindText = ourFindText[6:]
        compiledFindText = re.compile(ourFindText)
    else:
        replaceLen = len(ourReplaceText)
        #diffLen = replaceLen - searchLen
    #if optionsDict['ignoreDiacriticsFlag']: ourFindText = BibleOrgSysGlobals.removeAccents( ourFindText )
    #if optionsDict['caselessFlag']: ourFindText = ourFindText.lower()
    searchLen = len(ourFindText)
    if BibleOrgSysGlobals.debugFlag: assert searchLen
    #print( "  Searching for {!r} in {} loaded books".format( ourFindText, len(self) ) )

    if not self.preloadDone: self.preload()

    # The first entry in the result list is a dictionary containing the parameters
    #   Following entries are SimpleVerseKey objects
    encoding = self.encoding
    if encoding is None: encoding = 'utf-8'

    replaceAllFlag = stopFlag = undoFlag = False
    filesToSave = OrderedDict()
    if self.maximumPossibleFilenameTuples:
        for BBB, filename in self.maximumPossibleFilenameTuples:
            if optionsDict['bookList'] is None or optionsDict[
                    'bookList'] == 'ALL' or BBB in optionsDict['bookList']:
                #print( exp("findReplaceText: will search book {}").format( BBB ) )
                bookFilepath = os.path.join(self.sourceFolder, filename)
                with open(bookFilepath, 'rt', encoding=encoding) as bookFile:
                    bookText = bookFile.read()
                resultDict['searchedBookList'].append(BBB)

                #C = V = '0'
                if optionsDict['regexFlag']:  # ignores wordMode flag
                    ix = 0
                    while True:
                        match = compiledFindText.search(bookText, ix)
                        if not match: break  # none / no more found
                        ix, ixAfter = match.span()
                        regexFoundText = bookText[ix:ixAfter]
                        try:
                            regexReplacementText = compiledFindText.sub(
                                ourReplaceText, regexFoundText, count=1)
                        except re.error as err:
                            print("Search/Replace regex error: {}".format(err))
                            resultDict['hadRegexError'] = True
                            stopFlag = True
                            break
                        #print( "Found regex {!r} at {:,} in {}".format( ourFindText, ix, BBB ) )
                        #print( "  Found text was {!r}, replacement will be {!r}".format( regexFoundText, regexReplacementText ) )
                        resultDict['numFinds'] += 1
                        if BBB not in resultDict['foundBookList']:
                            resultDict['foundBookList'].append(BBB)

                        if optionsDict[
                                'contextLength']:  # Find the context in the original (fully-cased) string
                            contextBefore = bookText[
                                max(0, ix - optionsDict['contextLength']):ix]
                            contextAfter = bookText[
                                ixAfter:ixAfter + optionsDict['contextLength']]
                        else:
                            contextBefore = contextAfter = None
                        #print( "  After  {!r}".format( contextBefore ) )
                        #print( "  Before {!r}".format( contextAfter ) )

                        result = None
                        if not replaceAllFlag:
                            ref = BBB
                            willBeText = contextBefore + regexReplacementText + contextAfter
                            result = confirmCallback(
                                ref, contextBefore, regexFoundText,
                                contextAfter, willBeText,
                                resultDict['numReplaces'] > 0)
                            #print( "findReplaceText got", result )
                            assert result in 'YNASU'
                            if result == 'A': replaceAllFlag = True
                            elif result == 'S':
                                stopFlag = True
                                break
                            elif result == 'U':
                                undoFlag = True
                                break
                        if replaceAllFlag or result == 'Y':
                            #print( "  ix={:,}, ixAfter={:,}, diffLen={}".format( ix, ixAfter, diffLen ) )
                            bookText = bookText[:
                                                ix] + regexReplacementText + bookText[
                                                    ixAfter:]
                            ix += len(
                                regexReplacementText
                            )  # Start searching after the replacement
                            #print( "  ix={:,}, ixAfter={:,}, now={!r}".format( ix, ixAfter, bookText ) )
                            resultDict['numReplaces'] += 1
                            if BBB not in resultDict['replacedBookList']:
                                resultDict['replacedBookList'].append(BBB)
                            filesToSave[BBB] = (bookFilepath, bookText)
                        else:
                            ix += 1  # So don't keep repeating the same find
                else:  # not regExp
                    ix = 0
                    while True:
                        ix = bookText.find(ourFindText, ix)
                        if ix == -1: break  # none / no more found
                        #print( "Found {!r} at {:,} in {}".format( ourFindText, ix, BBB ) )
                        resultDict['numFinds'] += 1
                        if BBB not in resultDict['foundBookList']:
                            resultDict['foundBookList'].append(BBB)

                        ixAfter = ix + searchLen
                        if optionsDict['wordMode'] == 'Whole':
                            #print( "BF", repr(bookText[ix-1]) )
                            #print( "AF", repr(bookText[ixAfter]) )
                            if ix > 0 and bookText[ix - 1].isalpha():
                                ix += 1
                                continue
                            if ixAfter < len(
                                    bookText) and bookText[ixAfter].isalpha():
                                ix += 1
                                continue
                        elif optionsDict['wordMode'] == 'Begins':
                            if ix > 0 and bookText[ix - 1].isalpha():
                                ix += 1
                                continue
                        elif optionsDict['wordMode'] == 'EndsWord':
                            if ixAfter < len(
                                    bookText) and bookText[ixAfter].isalpha():
                                ix += 1
                                continue
                        elif optionsDict['wordMode'] == 'EndsLine':
                            if ixAfter < len(bookText):
                                ix += 1
                                continue

                        if optionsDict[
                                'contextLength']:  # Find the context in the original (fully-cased) string
                            contextBefore = bookText[
                                max(0, ix - optionsDict['contextLength']):ix]
                            contextAfter = bookText[
                                ixAfter:ixAfter + optionsDict['contextLength']]
                        else:
                            contextBefore = contextAfter = None
                        #print( "  After  {!r}".format( contextBefore ) )
                        #print( "  Before {!r}".format( contextAfter ) )

                        result = None
                        if not replaceAllFlag:
                            ref = BBB
                            willBeText = contextBefore + ourReplaceText + contextAfter
                            result = confirmCallback(
                                ref, contextBefore, ourFindText, contextAfter,
                                willBeText, resultDict['numReplaces'] > 0)
                            #print( "findReplaceText got", result )
                            assert result in 'YNASU'
                            if result == 'A': replaceAllFlag = True
                            elif result == 'S':
                                stopFlag = True
                                break
                            elif result == 'U':
                                undoFlag = True
                                break
                        if replaceAllFlag or result == 'Y':
                            #print( "  ix={:,}, ixAfter={:,}, diffLen={}".format( ix, ixAfter, diffLen ) )
                            bookText = bookText[:
                                                ix] + ourReplaceText + bookText[
                                                    ixAfter:]
                            ix += replaceLen  # Start searching after the replacement
                            #print( "  ix={:,}, ixAfter={:,}, now={!r}".format( ix, ixAfter, bookText ) )
                            resultDict['numReplaces'] += 1
                            if BBB not in resultDict['replacedBookList']:
                                resultDict['replacedBookList'].append(BBB)
                            filesToSave[BBB] = (bookFilepath, bookText)
                        else:
                            ix += 1  # So don't keep repeating the same find

            if stopFlag:
                if BibleOrgSysGlobals.verbosityLevel > 2:
                    print(
                        "Search/Replace was aborted in {} after {} replaces.".
                        format(BBB, resultDict['numReplaces']))
                resultDict['aborted'] = True
                break
            if undoFlag:
                if resultDict['numReplaces'] > 0:
                    if BibleOrgSysGlobals.verbosityLevel > 2:
                        print(
                            "Search/Replace was aborted in {} for undo in {} books."
                            .format(BBB, len(resultDict['replacedBookList'])))
                elif BibleOrgSysGlobals.verbosityLevel > 2:
                    print("Search/Replace was aborted (by undo) in {}.".format(
                        BBB))
                filesToSave = OrderedDict()
                resultDict['replacedBookList'] = []
                resultDict['numReplaces'] = 0
                resultDict['aborted'] = True
                break

    else:
        logging.critical(
            exp("No book files to search/replace in {}!").format(
                self.sourceFolder))

    for BBB, (filepath, fileText) in filesToSave.items():
        if optionsDict['doBackups']:
            if BibleOrgSysGlobals.verbosityLevel > 2:
                print("Making backup copy of {} file: {}…".format(
                    BBB, filepath))
            BibleOrgSysGlobals.backupAnyExistingFile(filepath, numBackups=4)
        if BibleOrgSysGlobals.verbosityLevel > 2:
            print("Writing {:,} bytes for {} to {}…".format(
                len(fileText), BBB, filepath))
        elif BibleOrgSysGlobals.verbosityLevel > 1:
            print("Saving {} with {} encoding".format(filepath, encoding))
        with open(filepath, 'wt', encoding=encoding,
                  newline='\r\n') as bookFile:
            bookFile.write(fileText)
        self.bookNeedsReloading[BBB] = True

    #print( exp("findReplaceText: returning {}/{}  {}/{}/{} books  {}").format( resultDict['numReplaces'], resultDict['numFinds'], len(resultDict['replacedBookList']), len(resultDict['foundBookList']), len(resultDict['searchedBookList']), optionsDict ) )
    return optionsDict, resultDict