Esempio n. 1
0
 def _open(self, fbts):
     # look for opening a .meta file
     if 'meta' == fbts.suffix():
         fb2 = utilities.file_less_suffix(fbts)
         if fb2 is None :
             m1 = _TR('File:Open','Cannot open a .meta file alone')
             m2 = _TR('File:Open','There is no book file matching ',
                      'filename follows this') + fbts.filename()
             utilities.warning_msg(m1, m2)
             return
         # we see foo.txt with foo.txt.meta, silently open it
         fbts = fb2
     # look for already-open file
     seq = self._is_already_open(fbts.fullpath())
     if seq is not None :
         self.focus_me(seq)
         return
     # start collecting auxiliary streams
     gw_stream = None
     bw_stream = None
     gg_stream = None
     # open the metadata stream, which is always UTF-8
     meta_stream = utilities.related_suffix(fbts, 'meta', encoding=C.ENCODING_UTF)
     if meta_stream is None :
         # opening book without .meta; look for .bin which is always LTN1
         bin_stream = utilities.related_suffix(fbts,'bin',encoding=C.ENCODING_LATIN)
         if bin_stream :
             gg_stream = metadata.translate_bin(bin_stream,fbts)
         # Look for good_words.txt, bad_words.txt.
         gw_stream = utilities.related_file( fbts, 'good_words*.*' )
         bw_stream = utilities.related_file( fbts, 'bad_words*.*' )
     seq = self.book_number
     # If the only open book is the new one created at startup or when all
     # books are closed (which will have key 0), and it has not been
     # modified, get rid of it.
     if len(self.open_books) == 1 \
     and 0 == list(self.open_books.keys())[0] \
     and self.open_books[0].get_book_name().startswith('Untitled-') \
     and not self.open_books[0].get_save_needed() :
         self.editview_tabset.clear()
         self.panel_tabset.clear()
         self.focus_book = None
         seq = 0
     else:
         # Some other book open, or user typed into the default New one.
         self.book_number += 1
     # Make the Book object and stow it in our open book dict
     a_book = book.Book( seq, self )
     self.open_books[seq] = a_book
     if meta_stream : # opening a book we previously saved
         a_book.old_book( fbts, meta_stream )
     else :
         a_book.new_book( fbts, gg_stream, gw_stream, bw_stream )
     index = self.editview_tabset.addTab(
         a_book.get_edit_view(), a_book.get_book_name())
     self.editview_tabset.setTabToolTip(index,
         a_book.get_book_folder() )
     self.focus_me(seq)
     self.last_open_path = fbts.folderpath() # start for next open or save
     self._add_to_recent(fbts.fullpath())
Esempio n. 2
0
def _do_parse( book, mainwindow ) :
    global WORK_UNITS
    import yapps_runtime

    edit_model = book.get_edit_model()
    scanner = DocScanner( edit_model.all_lines() )
    parser = dpdocsyntax.DPDOC( scanner )
    good_parse = False
    try:
        x = parser.goal()
        good_parse = True
    except yapps_runtime.SyntaxError as s:
        m1 = _TR('Checking document structure',
                 'Document structure error around line') + ' {}'
        m2 = 'Processing {}\n{}'.format( s.context.rule, s.msg )
        utilities.warning_msg(
            m1.format(s.pos[2]), info=m2, parent=mainwindow )
    except Exception as e:
        m1 = _TR('Checking document structure',
                 'Unknown error parsing document')
        m2 = str(e)
        xlt_logger.error(m1)
        xlt_logger.error(m2)
        utilities.warning_msg( m1, info=m2, parent=mainwindow )
    # whatever, clean up if there is a failure.
    if not good_parse :
        WORK_UNITS = []
    return good_parse
Esempio n. 3
0
 def old_book(self, doc_stream, meta_stream):
     self.book_name = doc_stream.filename()
     self.editv.book_renamed(self.book_name)
     self.book_folder = doc_stream.folderpath()
     self.book_full_path = doc_stream.fullpath()
     self.editm.setPlainText(doc_stream.readAll())
     meta_message = self.metamgr.load_meta(meta_stream)
     if meta_message:
         # Problem with json decoding of metadata file, warn the user
         m1 = _TR('error opening book',
                  'Error decoding the metadata file, metadata lost')
         m2 = _TR(
             'error opening book, details',
             'Recommend you close the book without saving and investigate the following:'
         )
         utilities.warning_msg(m1,
                               m2 + '\n' + meta_message,
                               parent=self.mainwindow)
     self.hook_images()
     # Everything loaded from a file, clear any mod status
     self.md_modified = 0
     self.editm.setModified(False)
     # Set the edit cursor to a saved location
     tc = self.editv.make_cursor(self.edit_cursor[0], self.edit_cursor[1])
     self.editv.center_this(tc)
Esempio n. 4
0
 def _save(self):
     active_book = self.open_books[self.focus_book]
     if active_book.get_save_needed():
         if active_book.get_book_name().startswith('Untitled-'):
             return self._save_as()
         doc_stream = utilities.path_to_output(
             active_book.get_book_full_path())
         if doc_stream:  # successfully opened for output
             meta_stream = utilities.related_output(doc_stream,
                                                    C.METAFILE_SUFFIX)
             if not meta_stream:
                 utilities.warning_msg(
                     _TR('File:Save',
                         'Unable to open metadata file for writing.'),
                     _TR('File:Save', 'Use loglevel=error for details.'),
                     parent=self)
                 return False
         else:
             utilities.warning_msg(_TR(
                 'File:Save', 'Unable to open book file for writing.'),
                                   _TR('File:Save',
                                       'Use loglevel=error for details.'),
                                   parent=self)
             return False
         return active_book.save_book(doc_stream, meta_stream)
Esempio n. 5
0
    def new_book(self, doc_stream, good_stream, bad_stream):
        self.book_name = doc_stream.filename()
        self.editv.book_renamed(self.book_name)
        self.book_folder = doc_stream.folderpath()
        self.book_full_path = doc_stream.fullpath()
        self.editm.setPlainText(doc_stream.readAll())
        self.editm.setModified(True)
        # If there are good_words and bad_words streams, call the worddata
        # metadata reader functions directly to accept them.
        if good_stream:
            self.wordm.good_file(good_stream)
        if bad_stream:
            self.wordm.bad_file(bad_stream)
        self.pagem.scan_pages()  # develop page metadata if possible
        self.hook_images()  # set up display of scan images if possible
        self.editv.set_cursor(self.editv.make_cursor(0, 0))  # cursor to top
        self._speller = dictionaries.Speller(self.dict_tag, self.book_folder)
        # Check the loaded text for \ufffd "replacement" chars indicating
        # mis-decoding of the file.
        findtc = self.editm.find(C.UNICODE_REPL, position=0)
        if not findtc.isNull():
            m1 = _TR(
                'File:Open finds bad encoding',
                'This document contains at least one Unicode Replacement Character!'
            )
            m2 = _TR(
                'File: Open finds bad encoding',
                '''This indicates it was read with the wrong encoding!
See the Help file under "File Encodings and File Names"
The first bad character is at ''', 'integer follows this') + str(
                    findtc.position())
            utilities.warning_msg(m1, m2, self.mainwindow)
Esempio n. 6
0
 def do_move(self):
     if not self._can_we_do_this() : return
     nzones = self.data_model.find_zones()
     if nzones == 0 :
         emsg = _TR(
             'Footnote panel error message',
             'Cannot move footnotes until footnote zones have been defined')
         expl = _TR(
             'Footnote panel explanation',
             'A Footnote zone is defined by "/F" and "F/" lines.' )
         utilities.warning_msg(emsg,expl,self)
         return
     # create a working cursor and start an undo macro on it.
     worktc = self.edit_view.get_cursor()
     worktc.beginEditBlock()
     # Do the actual work inside a try-finally block so as to be sure
     # that the Edit Block is ultimately closed.
     try :
         self.data_model.move_notes(worktc)
     except Exception as whatever:
         fnotview_logger.error(
             'Unexpected error moving footnotes: {}'.format(whatever.args)
             )
     worktc.endEditBlock()
     self.do_refresh()
Esempio n. 7
0
    def edit_book_facts(self):
        starting_text = ''
        for (key, arg) in self.book_facts.items():
            starting_text += '{} : {}\n'.format(key, arg)
        response = utilities.show_info_dialog(
            _TR(
                "Edit View Book Facts Dialog Title",
                '''Enter facts about the book such as Title or Author.\n
Each line must have a key such as Title, a colon, then a value.'''),
            self.editv, starting_text)
        if response:  # is not None, there is some text
            self.book_facts = dict()
            for line in response.split('\n'):
                if line.strip():  # is not empty,
                    try:
                        (key, arg) = line.split(
                            ':')  # exception if not exactly 1 colon
                        self.book_facts[key.strip()] = arg.strip()
                    except:
                        utilities.warning_msg(
                            _TR(
                                "Edit book-facts dialog warning message",
                                "Each line must have a key, a colon, and a value. Ignoring:"
                            ), "'{}'".format(line))
                # else skip empty line
            self.metadata_modified(True, C.MD_MOD_FLAG)
Esempio n. 8
0
 def _real_refresh(self):
     # Make sure we have access to the bookloupe executable
     bl_path = paths.get_loupe_path()
     if not bl_path : # path is null string
         bl_path = utilities.ask_executable(
             _TR('File-open dialog to select bookloupe',
                 'Select the bookloupe executable file'), self.parent() )
         if bl_path is None : # user selected non-executable
             utilities.warning_msg(
                 _TR('Error choosing bookloupe file',
                     'That is not an executable file.') )
             return
         if 0 == len(bl_path) : # user pressed Cancel
             return
         paths.set_loupe_path(bl_path)
     # bl_path is an executable, continue
     # create a temp file containing the book contents
     fbts = utilities.temporary_file()
     fbts << self.my_book.get_edit_model().full_text()
     fbts.rewind() # forces a flush()
     # create the bookloupe command
     command = [bl_path,'-d','-e','-t','-m']
     # line-end check is disabled by -l
     if not self.sw_l.isChecked() : command.append( '-l' )
     if self.sw_p.isChecked() : command.append( '-p' )
     if self.sw_s.isChecked() : command.append( '-s' )
     if self.sw_v.isChecked() : command.append( '-v' )
     if self.sw_x.isChecked() : command.append( '-x' )
     command.append( fbts.fullpath() )
     loupeview_logger.info('executing'+' '.join(command))
     # run it, capturing the output as a byte stream
     try:
         bytesout = subprocess.check_output( command, stderr=subprocess.STDOUT )
     except subprocess.CalledProcessError as CPE :
         msg1 = _TR('bookloupe call returns error code',
                 'Bookloupe execution ended with an error code')
         msg1 += ' '
         msg1 += str(CPE.returncode)
         msg2 = _TR('header for end of output string',
                    'Last part of bookloupe output:' )
         msg2 += '\n'
         msg2 += CPE.output[-100:].decode('UTF-8','replace')
         utilities.warning_msg( msg1, msg2, self.parent())
         return # leaving message_tuples empty
     # convert the bytes to unicode. bookloupe's message templates are
     # just ASCII but they can include quoted characters of any set.
     charsout = bytesout.decode(encoding='UTF-8',errors='replace')
     # convert the stream to a list of lines.
     linesout = charsout.split('\n')
     # process the lines into tuples in our list.
     for line in linesout :
         m = MSGREX.search(line)
         if m : # was matched, so is not None,
             lno = format( int(m.group(1)), ' >6' )
             c = 0 if m.group(3) is None else int(m.group(3))
             cno = format( c, ' >3' )
             msg = m.group(4)
             self.message_tuples.append( (lno, cno, msg) )
     loupeview_logger.info('loupeview total of {} items'.format(len(self.message_tuples)))
Esempio n. 9
0
 def do_insert(self):
     # Copy the text and if it is empty, complain and exit.
     ins_text = self.insert_text.text()
     if 0 == len(ins_text):
         utilities.warning_msg(
             _TR("Page Table warning message",
                 "No text to insert has been given"),
             _TR("Page Table warning message line 2",
                 "Write the text to insert in the field at the top."), self)
         return
     # See how many pages are involved, which is just the ones that aren't
     # marked skip. If no page info, or all are skip, complain and exit.
     n = 0
     for i in range(self.pdata.page_count()):
         if self.pdata.folio_info(i)[0] != C.FolioRuleSkip:
             n += 1
     if n == 0:  # page table empty or all rows marked skip
         utilities.warning_msg(
             _TR("Page Table warning message",
                 "No pages to insert text into."),
             _TR(
                 "Page Table warning message line 2",
                 "No page information is known, or all folios are set to 'Omit'."
             ), self)
         return
     # Get permission to do this significant operation.
     ok = utilities.ok_cancel_msg(
         _TR("Page Table permission request",
             "OK to insert the following string into %n pages?",
             n=n), ins_text, self)
     if ok:
         # get a cursor on the edit document.
         tc = self.my_book.get_edit_view().get_cursor()
         # Start a single undo-able operation on that cursor
         tc.beginEditBlock()
         # Working from the end of the document backward, go to the
         # top of each page and insert the string
         for i in reversed(range(self.pdata.page_count())):
             [rule, fmt, val] = self.pdata.folio_info(i)
             if rule != C.FolioRuleSkip:
                 # Note the page's start position and set our work cursor to it
                 pos = self.pdata.position(i)
                 tc.setPosition(pos)
                 # Copy the insert string, replacing %f with this folio
                 # and %i with the image filename.
                 f_str = self.pdata.folio_string(i)
                 i_str = self.pdata.filename(i)
                 temp = ins_text.replace('%f', f_str).replace('%i', i_str)
                 # Insert that text at the position of the start of this page.
                 tc.insertText(temp)
                 # The insertion goes in ahead of the saved cursor
                 # position so now it points after the inserted string --
                 # effectively, the insert has gone to the end of the
                 # prior page not the start of this one. Put the cursor
                 # for this page back where it was, thus preceding the
                 # inserted text.
                 self.pdata.set_position(i, pos)
         tc.endEditBlock()  # wrap up the undo op
Esempio n. 10
0
 def _can_we_do_this(self):
     self.do_refresh()
     m = self.data_model.mismatches()
     if (m) :
         emsg = _TR(
             'Footnote panel error message',
             'Cannot do this action when there are unmatched Anchors and Notes.'
             )
         expl = _TR(
             'Footnote panel info message',
             'There are %n unmatched Anchors and Notes.',n=m)
         utilities.warning_msg(emsg,expl,self)
     return m == 0
Esempio n. 11
0
 def _can_we_do_this(self):
     self.do_refresh()
     m = self.data_model.mismatches()
     if (m):
         emsg = _TR(
             'Footnote panel error message',
             'Cannot do this action when there are unmatched Anchors and Notes.'
         )
         expl = _TR('Footnote panel info message',
                    'There are %n unmatched Anchors and Notes.',
                    n=m)
         utilities.warning_msg(emsg, expl, self)
     return m == 0
Esempio n. 12
0
 def add_words(self, word_list):
     if len(word_list) <= 20 :
         self.beginResetModel()
         for word in word_list:
             self.words.add_to_good_set(word)
         self.get_data()
         self.endResetModel()
         return True
     # Too many words, save user from an ugly mistake.
     utilities.warning_msg(
         _TR('Good-word list drop error',
             'You may not drop more than 20 words at one time on the Good Words list'),
         _TR('Good-word list drop error explanation',
             'There are %n words in the clipboard. This is probably a mistake.',n=len(word_list)),
         self.save_parent)
     return False
Esempio n. 13
0
 def _save(self):
     active_book = self.open_books[self.focus_book]
     if active_book.get_save_needed() :
         if active_book.get_book_name().startswith('Untitled-'):
             return self._save_as()
         doc_stream = utilities.path_to_output( active_book.get_book_full_path() )
         if doc_stream : # successfully opened for output
             meta_stream = utilities.related_output(doc_stream,'meta')
             if not meta_stream:
                 utilities.warning_msg(
                     _TR('File:Save', 'Unable to open metadata file for writing.'),
                     _TR('File:Save', 'Use loglevel=error for details.') )
                 return False
         else:
             utilities.warning_msg(
                 _TR('File:Save', 'Unable to open book file for writing.'),
                 _TR('File:Save', 'Use loglevel=error for details.') )
             return False
         return active_book.save_book(doc_stream, meta_stream)
Esempio n. 14
0
 def _read_hash(self, sentinel, value, version):
     if version < '2':
         try:
             value = bytes(value, 'Latin-1', 'ignore')
         except:
             self.logger.error(
                 'Could not convert dochash to bytes, ignoring dochash')
             return
     # If the saved and current hashes now disagree, it is because the metadata
     # was saved along with a different book or version. Warn the user.
     if self._signature() != value:
         self.logger.error(
             'Doc hash in metadata does not match book contents')
         utilities.warning_msg(
             text=_TR('Book object',
                      'Document and .meta files do not match!',
                      'Warning during File:Open'),
             info=_TR(
                 'Book object',
                 'Page breaks and other metadata may be wrong! Strongly recommend you not edit or save this book.',
                 'Warning during File:Open'),
             parent=self.mainwindow)
Esempio n. 15
0
 def do_move(self):
     if not self._can_we_do_this(): return
     nzones = self.data_model.find_zones()
     if nzones == 0:
         emsg = _TR(
             'Footnote panel error message',
             'Cannot move footnotes until footnote zones have been defined')
         expl = _TR('Footnote panel explanation',
                    'A Footnote zone is defined by "/F" and "F/" lines.')
         utilities.warning_msg(emsg, expl, self)
         return
     # create a working cursor and start an undo macro on it.
     worktc = self.edit_view.get_cursor()
     worktc.beginEditBlock()
     # Do the actual work inside a try-finally block so as to be sure
     # that the Edit Block is ultimately closed.
     try:
         self.data_model.move_notes(worktc)
     except Exception as whatever:
         fnotview_logger.error(
             'Unexpected error moving footnotes: {}'.format(whatever.args))
     worktc.endEditBlock()
     self.do_refresh()
Esempio n. 16
0
 def ask_dictionary(self):
     tag_list = dictionaries.get_tag_list(self.book_folder)
     if 0 < len(tag_list):
         # dictionaries knows about at least one tag, display it/them
         item_list = sorted(list(tag_list.keys()))
         current = 0
         if self.dict_tag in item_list:
             current = item_list.index(self.dict_tag)
         title = _TR("EditViewWidget", "Primary dictionary for this book",
                     "Dictionary pop-up list")
         explanation = _TR(
             "EditViewWidget",
             "Select the best dictionary for spell-checking this book",
             "Dictionary pop-up list")
         new_tag = utilities.choose_from_list(title,
                                              explanation,
                                              item_list,
                                              parent=self.editv,
                                              current=current)
         if (new_tag is not None) and (new_tag != self.dict_tag):
             # a choice was made and it's different from before
             self.dict_tag = new_tag
             self._speller = dictionaries.Speller(new_tag,
                                                  tag_list[new_tag])
             self.wordm.recheck_spelling(self._speller)
             return True
     else:
         # no known dictionaries, probably the Extras have not been
         # configured -- tell user.
         utilities.warning_msg(
             text=_TR("EditViewWidget", "No dictionaries found",
                      "Dictionary request warning"),
             info=_TR("EditViewWidget",
                      "Perhaps the 'extras' folder is not defined?'",
                      "Dictionary request info"))
     return False
Esempio n. 17
0
assert fbfoo.filename() == f_foo
fbfoo.writeLine('some data')
assert os.path.isfile(foo_path)
fbbar = utilities.related_output(fbfoo, 'bar')
assert fbbar.filename() == f_bar
fbbar.writeLine('some data')
assert os.path.isfile(bar_path)

# Message routines
utilities.info_msg('About to beep', 'When you dismiss this message')
utilities.beep()
assert utilities.ok_cancel_msg('Did you hear a beep?',
                               'OK for yes, Cancel for no',
                               parent=T.main)
utilities.warning_msg('This is a warning',
                      'the very last warning',
                      parent=T.main)
assert utilities.save_discard_cancel_msg('Click SAVE', 'no parent argument')
assert not utilities.save_discard_cancel_msg(
    'Click DISCARD (dont save?)', 'does have parent', parent=T.main)
assert utilities.save_discard_cancel_msg('Click CANCEL', 'no parent') is None
result = utilities.get_find_string('Press CANCEL', T.main, prepared='preptext')
assert result is None
preptext = 'REPLACE-WITH-FOO'
result = utilities.get_find_string('Press Find', T.main, prepared=preptext)
assert result == preptext

# to_roman, to_alpha
assert 'mmmmcmxcix' == utilities.to_roman(4999)
assert 'i' == utilities.to_roman(1)
assert 'XIV' == utilities.to_roman(14, lc=False)
Esempio n. 18
0
 def _open(self, fbts):
     # look for opening a .META file
     if C.METAFILE_SUFFIX == fbts.suffix():
         fb2 = utilities.file_less_suffix(fbts)
         if fb2 is None:
             m1 = _TR('File:Open', 'Cannot open a metadata file alone')
             m2 = _TR('File:Open', 'There is no book file matching ',
                      'filename follows this') + fbts.filename()
             utilities.warning_msg(m1, m2, parent=self)
             return
         # we see foo.txt with foo.txt.META, silently open it
         fbts = fb2
     # look for already-open file
     seq = self._is_already_open(fbts.fullpath())
     if seq is not None:
         self.focus_me(seq)
         return
     # start collecting auxiliary streams
     gw_stream = None
     bw_stream = None
     #gg_stream = None
     # open the metadata stream, which is always UTF-8
     meta_stream = utilities.related_suffix(fbts,
                                            C.METAFILE_SUFFIX,
                                            encoding=C.ENCODING_UTF)
     if meta_stream is None:
         # opening book without metadata; look for .bin which is always LTN1
         # This is no longer supported - somebody who cares, can write a
         # .bin-to-JSON utility if they want.
         #bin_stream = utilities.related_suffix(fbts,'bin',encoding=C.ENCODING_LATIN)
         #if bin_stream :
         #gg_stream = metadata.translate_bin(bin_stream,fbts)
         # Look for good_words.txt, bad_words.txt.
         gw_stream = utilities.related_file(fbts, 'good_words*.*')
         bw_stream = utilities.related_file(fbts, 'bad_words*.*')
     seq = self.book_number
     # If the only open book is the new one created at startup or when all
     # books are closed (which will have key 0), and it has not been
     # modified, get rid of it.
     if len(self.open_books) == 1 \
     and 0 == list(self.open_books.keys())[0] \
     and self.open_books[0].get_book_name().startswith('Untitled-') \
     and not self.open_books[0].get_save_needed() :
         self.editview_tabset.clear()
         self.panel_tabset.clear()
         self.focus_book = None
         seq = 0
     else:
         # Some other book open, or user typed into the default New one.
         self.book_number += 1
     # Make the Book object and stow it in our open book dict
     a_book = book.Book(seq, self)
     self.open_books[seq] = a_book
     if meta_stream:  # opening a book we previously saved
         a_book.old_book(fbts, meta_stream)
     else:
         a_book.new_book(fbts, gw_stream, bw_stream)
     index = self.editview_tabset.addTab(a_book.get_edit_view(),
                                         a_book.get_book_name())
     self.editview_tabset.setTabToolTip(index, a_book.get_book_folder())
     self.focus_me(seq)
     self.last_open_path = fbts.folderpath()  # start for next open or save
     self._add_to_recent(fbts.fullpath())
Esempio n. 19
0
if os.path.isfile(bar_path):
    os.remove(bar_path)
fbfoo = utilities.path_to_output(foo_path)
assert fbfoo.filename() == f_foo
fbfoo.writeLine('some data')
assert os.path.isfile(foo_path)
fbbar = utilities.related_output(fbfoo,'bar')
assert fbbar.filename() == f_bar
fbbar.writeLine('some data')
assert os.path.isfile(bar_path)

# Message routines
utilities.info_msg('About to beep', 'When you dismiss this message')
utilities.beep()
assert utilities.ok_cancel_msg('Did you hear a beep?', 'OK for yes, Cancel for no',parent=T.main)
utilities.warning_msg('This is a warning','the very last warning',parent=T.main)
assert utilities.save_discard_cancel_msg('Click SAVE','no parent argument')
assert not utilities.save_discard_cancel_msg('Click DISCARD (dont save?)','does have parent',parent=T.main)
assert utilities.save_discard_cancel_msg('Click CANCEL','no parent') is None
result = utilities.get_find_string('Press CANCEL',T.main,prepared='preptext')
assert result is None
preptext = 'REPLACE-WITH-FOO'
result = utilities.get_find_string('Press Find',T.main,prepared=preptext)
assert result==preptext

# to_roman, to_alpha
assert 'mmmmcmxcix' == utilities.to_roman(4999)
assert 'i' == utilities.to_roman(1)
assert 'XIV' == utilities.to_roman(14, lc=False)
emsg = 'Invalid number for roman numeral'
assert '????' == utilities.to_roman(5000)
Esempio n. 20
0
def xlt_book( source_book, xlt_index, main_window ) :
    global WORK_UNITS

    # Get the namespace of the chosen Translator, based on the index saved in
    # the menu action.
    xlt_namespace = _XLT_NAMESPACES[ xlt_index ]
    menu_name = getattr( xlt_namespace, 'MENU_NAME' )
    xlt_logger.info('Translating {}: {}'.format(xlt_index, menu_name) )

    # If it has an option dialog, now is the time to run it. If the
    # user clicks Cancel, we are done.
    dialog_list = getattr( xlt_namespace, 'OPTION_DIALOG', None )
    if dialog_list :
        answer = _run_dialog( dialog_list, menu_name, main_window )
        if not answer :
            xlt_logger.error('User cancelled option dialog for', menu_name)
            return None

    # Perform the document parse. If it succeeds, the list of work units is
    # ready. If it fails, a message has been shown to the user and we exit.
    WORK_UNITS = []
    if not _do_parse( source_book, main_window ) :
        return None

    # Initialize the translator. Create three streams. Collect the book facts
    # dict. Make a page boundary offset list filled with -1. Pass all that to
    # the initialize function to store.

    prolog = utilities.MemoryStream()
    body = utilities.MemoryStream()
    epilog = utilities.MemoryStream()
    book_facts = source_book.get_book_facts()
    source_page_model = source_book.get_page_model()
    page_list = []
    if source_page_model.active() :
        page_list = [ -1 for x in range( source_page_model.page_count() ) ]

    try:
        result = xlt_namespace.initialize( prolog, body, epilog, book_facts, page_list )
    except Exception as e :
        m1 = _TR( 'Translator throws exception',
                  'Unexpected error initializing translator' ) + ' ' + menu_name
        m2 = str(e)
        utilities.warning_msg( m1, m2, main_window )
        xlt_logger.error('Exception from {}.initialize()'.format(menu_name))
        xlt_logger.error(m2)
        return None
    if not result : return None

    # The translator is initialized, so call its translate() passing our
    # event_generator(), below.

    try:
        event_iterator = event_generator( source_page_model, source_book.get_edit_model() )
        result = xlt_namespace.translate( event_iterator )
    except Exception as e :
        m1 = _TR( 'Translator throws exception',
                  'Unexpected error in translate() function of') + ' ' + menu_name
        m2 = str(e)
        utilities.warning_msg( m1, m2, main_window )
        xlt_logger.error('Exception from {}.translate()'.format(menu_name))
        xlt_logger.error(m2)
        return None
    if not result : return None

    # Translating over, finalize it.

    try:
        result = xlt_namespace.finalize( )
    except Exception as e :
        m1 = _TR( 'Translator throws exception',
                  'Unexpected error in finalize() function of') + ' ' + menu_name
        m2 = str(e)
        utilities.warning_msg( m1, m2, main_window )
        xlt_logger.error('Exception from {}.finalize()'.format(menu_name))
        xlt_logger.error(m2)
        return None
    if not result : return None

    # Now put it all together as a Book. First, have mainwindow create a
    # New book and display it.

    new_book = main_window.do_new()

    # Get an edit cursor and use it to insert all the translated text.

    new_edit_view = new_book.get_edit_view()
    qtc = new_edit_view.get_cursor()
    prolog.rewind()
    qtc.insertText( prolog.readAll() )
    body.rewind()
    qtc.insertText( body.readAll() )
    epilog.rewind()
    qtc.insertText( epilog.readAll() )

    # Position the file at the top.

    new_edit_view.go_to_line_number( 1 )

    # Read relevant metadata sections from the source book and install them
    # on the new book. metadata.write_section() gets a specific section.
    # metadata.load_meta() doesn't care if the stream is a single section
    # or a whole file.

    source_mgr = source_book.get_meta_manager()
    new_mgr = new_book.get_meta_manager()

    new_book.book_folder = source_book.book_folder # base folder
    _move_meta( source_mgr, new_mgr, C.MD_BW ) # badwords
    _move_meta( source_mgr, new_mgr, C.MD_FR ) # find memory
    _move_meta( source_mgr, new_mgr, C.MD_FU ) # find userbuttons
    _move_meta( source_mgr, new_mgr, C.MD_GW ) # goodwords
    _move_meta( source_mgr, new_mgr, C.MD_MD ) # main dictionary
    _move_meta( source_mgr, new_mgr, C.MD_NO ) # notes
    if source_page_model.active() and page_list[0] > -1 :
        # It looks as if the translator did update the page offset list. Get
        # the source page metadata and modify it. (Note the original design
        # was to just transfer the old page metadata the way we transfer the
        # other types, above, then use pagedata.set_position() to set the new
        # positions. HOWEVER there is a possibility that the new book is
        # shorter than the old one, in which case pagedata.read_pages() would
        # see some of the (old) positions as invalid, and would discard some
        # of the final rows. So we get the metadata; translate it back to
        # python; modify it in place with the new positions which are
        # presumably all valid for the new book; convert it to json and feed
        # that to the new book. If the translator messed up any of those
        # positions, there will be log messages.
        stream = utilities.MemoryStream()
        source_mgr.write_section( stream, C.MD_PT )
        stream.rewind()
        pdata = json.loads( stream.readAll() )
        plist = pdata['PAGETABLE']
        for j in range( len( plist ) ) :
            plist[j][0] = page_list[j]
        stream = utilities.MemoryStream()
        stream << json.dumps( pdata )
        stream.rewind()
        new_mgr.load_meta( stream )
        new_book.hook_images()