Beispiel #1
0
    def getArticles(self):
        if not self.accessToken:
            self.accessToken = self._authenticate()

        if not self.accessToken:
            showCritical('Authentication failed.')
            return []

        response = post(
            'https://getpocket.com/v3/get',
            json={
                'consumer_key': self.consumerKey,
                'access_token': self.accessToken,
                'contentType': 'article',
                'count': 30,
                'detailType': 'complete',
                'sort': 'newest',
            },
            headers=self.headers,
        )

        if response.json()['list']:
            return [{
                'text': a['resolved_title'],
                'data': a
            } for a in response.json()['list'].values()]

        showInfo('You have no unread articles remaining.')
        return []
Beispiel #2
0
    def _windowsSetMimeData(self, clip, mime):
        import win32clipboard, time
        from ctypes import windll

        grabbed = False
        for i in range(10):
            try:
                win32clipboard.OpenClipboard(self.editor.parentWindow.winId())
                grabbed = True
                break
            except:
                time.sleep(0.01)
                continue

        if not grabbed:
            hwnd = win32clipboard.GetOpenClipboardWindow()

            longpid = ctypes.c_ulong()
            result = windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(longpid))
            pid = longpid.value

            showCritical(_("Unable to access clipboard - locked by process %d" % pid))
            return

        win32clipboard.CloseClipboard()
        clip.setMimeData(mime)
Beispiel #3
0
 def test_executable(self) -> None:
     "Check to see if the TiddlyWiki executable provided can be called from Anki."
     # pylint: disable=no-member
     QApplication.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
     try:
         args = [self.form.tiddlywikiBinary_.text(), "--version"]
         proc = subprocess.run(args,
                               check=True,
                               stderr=subprocess.STDOUT,
                               stdout=subprocess.PIPE,
                               startupinfo=nowin_startupinfo())
     except FileNotFoundError:
         QApplication.restoreOverrideCursor()
         showCritical(
             "It doesn't look like that file exists on your computer. "
             "Try using the full path to 'tiddlywiki'.")
     except subprocess.CalledProcessError as e:
         QApplication.restoreOverrideCursor()
         showCritical(
             f"It's not quite working yet. Try seeing if you can run TiddlyWiki "
             f"from the command line and copy your command in here.\n\n"
             f"{e.output}")
     except Exception:
         QApplication.restoreOverrideCursor()
         raise
     else:
         QApplication.restoreOverrideCursor()
         showInfo(
             f"Successfully called TiddlyWiki {proc.stdout.decode().strip()}! "
             f"You're all set.")
Beispiel #4
0
 def onLoginFailed(self):
     showCritical('登录失败!')
     self.tabWidget.setCurrentIndex(1)
     self.progressBar.setValue(0)
     self.progressBar.setMaximum(1)
     self.mainTab.setEnabled(True)
     self.cookieLineEdit.clear()
    def saveConfig(self):
        if not self.ui.deckComboBox.currentText():
            self.log('deck为空')
            showCritical('Deck不能为空')
            return False
        elif not self.ui.username.text() or not self.ui.password.text():
            self.log('用户名或密码为空')
            showCritical('未登录')
            self.ui.tabWidget.setCurrentIndex(1)
            return False

        index = self.ui.dictionaryComboBox.currentIndex()
        self._config['active'] = index
        self._config['dictionaries'][index][
            'username'] = self.ui.username.text()
        self._config['dictionaries'][index][
            'password'] = self.ui.password.text()
        self._config['deck'] = self.ui.deckComboBox.currentText()
        self._config['image'] = self.ui.imageCheckBox.isChecked()
        self._config['sample'] = self.ui.samplesCheckBox.isChecked()
        self._config['BrEPron'] = self.ui.BrEPronCheckBox.isChecked()
        self._config['AmEPron'] = self.ui.AmEPronCheckBox.isChecked()
        self._config['BrEPhonetic'] = self.ui.BrEPhoneticCheckBox.isChecked()
        self._config['AmEPhonetic'] = self.ui.AmEPhoneticCheckBox.isChecked()
        guardConfig = deepcopy(self._config)
        for i in range(len(guardConfig['dictionaries'])):
            guardConfig['dictionaries'][i]['username'] = '******'
            guardConfig['dictionaries'][i]['password'] = '******'
            guardConfig['dictionaries'][i]['cookie'] = '*******'
        self.log(f'保存配置项:\n{json.dumps(guardConfig, indent=4)}')
        mw.addonManager.writeConfig(__name__, self._config)
        return True
Beispiel #6
0
    def handle_sync_finished(self) -> None:
        """Handle sync finished.

        In case of any error - show error message in manual mode and do nothing
        otherwise.  If no error - save the collection and show sync statistics
        in manual mode.  If `self._remove_obsolete_on_sync` is True - remove
        all notes that is not added or updated in current sync.
        """
        assert self.notes_manager  # mypy
        assert self.collection  # mypy
        self._alive_workers -= 1
        if self._alive_workers:
            return
        self.notion_menu.setTitle('Notion')
        # Show errors if manual sync
        if self._sync_errors:
            if not self._is_auto_sync:
                error_msg = '\n'.join(self._sync_errors)
                showCritical(error_msg, title='Loading from Notion failed')
        # If no errors - save collection and refresh Anki window
        else:
            if self._remove_obsolete_on_sync:
                ids_to_remove = self.existing_note_ids - self.synced_note_ids
                if ids_to_remove:
                    msg = (
                        f'Will delete {len(ids_to_remove)} obsolete note(s), '
                        f'continue?')
                    do_delete = QMessageBox.question(
                        mw,
                        'Confirm deletion',
                        msg,
                        QMessageBox.Yes | QMessageBox.No,
                    )
                    if do_delete == QMessageBox.Yes:
                        self.notes_manager.remove_notes(ids_to_remove)
                        self._deleted += len(ids_to_remove)
            self.collection.save(trx=False)
            mw.maybeReset()  # type: ignore[union-attr]
            mw.deckBrowser.refresh()  # type: ignore[union-attr]
            stats = (f'Processed: {self._processed}\n'
                     f'Created: {self._created}\n'
                     f'Updated: {self._updated}\n'
                     f'Deleted: {self._deleted}')
            if not self._is_auto_sync:
                showInfo(
                    f'Successfully loaded:\n{stats}',
                    title='Loading from Notion',
                )
        self.logger.info(
            'Sync finished, processed=%s, created=%s, updated=%s, deleted=%s',
            self._processed,
            self._created,
            self._updated,
            self._deleted,
        )
        self._reset_stats()
Beispiel #7
0
 def onLoginFailed(self):
     showCritical('第一次登录或cookie失效!请重新登录')
     self.progressBar.setValue(0)
     self.progressBar.setMaximum(1)
     self.mainTab.setEnabled(True)
     self.loginDialog = LoginDialog(
         loginUrl=self.api.loginUrl,
         loginCheckCallbackFn=self.api.loginCheckCallbackFn,
         parent=self)
     self.loginDialog.loginSucceed.connect(self.onLoginSuccess)
     self.loginDialog.show()
Beispiel #8
0
    def reload_config(self, new_config: Dict[str, Any]) -> None:
        """Reload configuration.

        :param new_config: new configuration
        """
        try:
            self._validate_config(new_config)
        except ValidationError as exc:
            self.logger.error('Config update error', exc_info=exc)
            showCritical(str(exc), title='Notion loader config update error')
        else:
            self.config = new_config
def timesUp():
    global death_toll
    death_toll -= 1
    if not death_toll:
        showCritical("Death Toll Was Immeasurable...",
                     title="Goodbye Cruel World!")
        if ANKI21:
            mw.unloadProfileAndExit()
        else:
            sys.exit()  #freezes on A21

    did = mw.col.decks.selected()
    conf = mw.col.decks.confForDid(did)
    hp_recover = conf.get('maxLife', 120) // 2
    runHook('LifeDrain.recover', True, hp_recover)
Beispiel #10
0
    def get_valid_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
        """Get valid configuration.

        :param config: configuration
        :returns: either configuration provided (if it's valid) or default
            config
        """
        try:
            self._validate_config(config)
        except ValidationError as exc:
            showCritical(str(exc), title='Notion loader config load error')
            assert mw  # mypy
            default_config = mw.addonManager.addonConfigDefaults(str(BASE_DIR))
            return cast(Dict[str, Any], default_config)
        else:
            return config
Beispiel #11
0
def getWKAvailableSubjectIds(type):
    url = WK_ASSIGNMENT_URL.format(type)
    retVal = set()
    while url:
        assignments = callWaniKani(url)
        if not assignments:
            showCritical(
                "No response from WaniKani, the deck is partially filled.")
            break

        retVal = retVal | set(x[u'data'][u'subject_id']
                              for x in assignments[u'data']
                              if x[u'data'][u'srs_stage'] > 0)
        url = assignments[u'pages'][u'next_url']

    return retVal
Beispiel #12
0
def on_run_ocr(browser: Browser):
    selected_nids = browser.selectedNotes()
    num_notes = len(selected_nids)
    config = mw.addonManager.getConfig(__name__)
    if num_notes == 0:
        showInfo("No cards selected.")
        return
    elif askUser(
            f"Are you sure you wish to run OCR processing on {num_notes} notes?"
    ) is False:
        return

    if config.get("tesseract_install_valid") is not True and config.get(
            "text_output_location") == "new_field":
        showInfo(
            f"Note that because this addon changes the note template, you will see a warning about changing the database and uploading to AnkiWeb. \n"
            f"This is normal, and will be shown each time you modify a note template.\n"
            f"This message will be only be shown once.")
        mw.addonManager.writeConfig(__name__, config)

    config[
        "tesseract_install_valid"] = True  # Stop the above msg appearing multiple times

    progress = mw.progress
    ocr = OCR(col=mw.col, progress=progress, languages=config["languages"])
    progress.start(immediate=True, min=0, max=num_notes)
    try:
        ocr.run_ocr_on_notes(note_ids=selected_nids,
                             overwrite_existing=config["overwrite_existing"])
        progress.finish()
        showInfo(f"Processed OCR for {num_notes} cards")

    except pytesseract.TesseractNotFoundError:
        progress.finish()
        showCritical(
            text=f"Could not find a valid Tesseract-OCR installation! \n"
            f"Please visit the addon page in at https://ankiweb.net/shared/info/450181164 for"
            f" install instructions")
    except Exception as errmsg:
        progress.finish()
        showCritical(
            f"Error encountered during processing, attempting to stop AnkiOCR gracefully. Error below:\n"
            f"{errmsg}")
    finally:
        browser.model.reset()
        mw.requireReset()
Beispiel #13
0
def updateWKKanjiDeck():

    # select deck
    deckId = mw.col.decks.id(WK_CONF['WK Kanji Deck Name'])
    mw.col.decks.select(deckId)
    # anki defaults to the last note type used in the selected deck
    model = mw.col.models.byName(WK_CONF['WK Kanji Model Name'])
    deck = mw.col.decks.get(deckId)
    deck['mid'] = model['id']
    mw.col.decks.save(deck)
    # set the last deck used by this note type -» puts this note in the last used deck
    model['did'] = deckId
    mw.col.models.save(model)

    NumONotes = len(
        mw.col.findNotes("'note:{}'".format(WK_CONF['WK Kanji Model Name'])))

    # let import the notes page by page
    # just the available levels
    # be careful the WK_KANJI_URL contains the type parameter so the levels is added to by &
    availableKanjiIds = getWKAvailableSubjectIds("kanji")
    url = WK_KANJI_URL + "&" + WKH_LEVELS_KEY + "=" + ",".join(
        str(x) for x in list(range(1,
                                   getWKMaxLevel() + 1)))
    while url:
        kanjiJson = callWaniKani(url)
        if not kanjiJson:
            showCritical(
                "No response from WaniKani, the deck is partially filled.")
            break
        wki = WKKanjiImporter(mw.col, kanjiJson)
        # ignore if first field matches existing note (0 update - default, 2 import)
        wki.importMode = 1
        wki.availableIds = availableKanjiIds
        wki.initMapping()
        wki.run()
        url = kanjiJson[u'pages'][u'next_url']

    mw.app.processEvents()
    mw.deckBrowser.show()
    showInfo(
        "WaniKani Kanji Syncroniser added {} new kanji(s) to Anki.".format(
            len(
                mw.col.findNotes("'note:{}'".format(
                    WK_CONF['WK Kanji Model Name']))) - NumONotes))
Beispiel #14
0
def check_note_type():
    mm = aqt.mw.col.models
    m = mm.byName(Textograph_MODEL_NAME)
    model_versioned_name = f"{Textograph_MODEL_NAME} v{TG_MODEL_VERSION['major']}"
    if not m:
        model_CreateOrRename(mm, model_versioned_name)
        check_note_type()
    else:
        try:
            v_major, v_minor = m['ver'].split(".")
            ver_dif = int(TG_MODEL_VERSION['major']) - int(v_major)
            if ver_dif == 0:
                if int(v_minor) < int(TG_MODEL_VERSION['minor']):
                    # update current model
                    t = m['tmpls'][0]
                    m["css"] += template.get_css()
                    t["qfmt"] = template.create_frontside()
                    t["afmt"] = template.create_backside()
                    m['ver'] = TG_STR_VERSION
                    mm.save()
            else:
                # if there is another model with same version?
                # swap model names in this way:
                # Textograph --> Textograph v1, Textograph v2 ---> Textograph
                m['name'] = f"{Textograph_MODEL_NAME} v{v_major}"
                mm.save(m, updateReqs=False)
                model_CreateOrRename(mm, model_versioned_name)
                check_note_type(
                )  # call again to check for minor version compatibility or other problems
                if ver_dif > 0:
                    showInfo(
                        f"Textograph Note Type changed to version: {TG_STR_VERSION}"
                        f", Previous version was {m['ver']}")
                else:
                    showCritical(
                        "Current Textograph model works with new Textograph Add-on version. "
                        "So Please! update your addon, You can continue to review them but creating new cards "
                        "from them is not possible. Although you can create new notes ant it works "
                        "fine with them.")

        except (ValueError, NameError, KeyError):
            showCritical(
                "there was an error when updating note type. renaming these note types may solve the problem:"
                f"{Textograph_MODEL_NAME} or {model_versioned_name} ")
Beispiel #15
0
def get_audio(editor):

    if 'front' == default_text_source.lower():
        source_text = editor.note.fields[0].lower()
        editor.web.eval("focusField(%d);" %
                        0)  #set focus to field where media will be added
    else:
        source_text = editor.note.fields[1].lower()
        editor.web.eval("focusField(%d);" %
                        1)  #set focus to field where media will be added

    source_text = source_text.replace('&nbsp;', ' ')

    sot = SoundOfText()
    result = sot.order_conversion(source_text, default_audio_language)

    if result['success']:
        editor.mw.progress.start(label=_("Downloading..."), immediate=True)
        audio_id = result['id']
        audio_url = None
        try:
            audio_url = sot.get_audio_url(audio_id)
        except Exception as e:
            editor.mw.progress.finish()
            showCritical(str(e), title='Text2Audio download error')
        else:
            #download MP3 file
            download_result = sot.download_file(audio_url)
            #rename file before copy
            old_file_name = download_result[0]
            media_folder = os.path.join(editor.mw.pm.profileFolder(),
                                        "collection.media")
            new_file_name = create_new_file_name(old_file_name, source_text,
                                                 media_folder)
            if new_file_name:
                os.rename(old_file_name, new_file_name)
                #save to media folder, set audio in note and remove original file
                editor.addMedia(new_file_name, canDelete=True)
            else:
                #remove downloaded file
                remove_uploaded_file(old_file_name)
            editor.mw.progress.finish()
    else:
        showCritical(result['message'], title='Text2Audio download error')
Beispiel #16
0
    def importWebpage(self, url=None, priority=None, silent=False):
        if not url:
            url, accepted = getText('Enter URL:', title='Import Webpage')
        else:
            accepted = True

        if not url or not accepted:
            return

        if not urlsplit(url).scheme:
            url = 'http://' + url
        elif urlsplit(url).scheme not in ['http', 'https']:
            showCritical('Only HTTP requests are supported.')
            return

        try:
            webpage = self._fetchWebpage(url)
        except HTTPError as error:
            showWarning(
                'The remote server has returned an error: '
                'HTTP Error {} ({})'.format(error.code, error.reason)
            )
            return
        except ConnectionError as error:
            showWarning('There was a problem connecting to the website.')
            return

        body = '\n'.join(map(str, webpage.find('body').children))
        source = self.settings['sourceFormat'].format(
            date=date.today(),
            url='<a href="%s">%s</a>' % (url, url)
        )

        if self.settings['prioEnabled'] and not priority:
            priority = self._getPriority(webpage.title.string)

        deck = self._createNote(webpage.title.string, body, source, priority)

        if not silent:
            tooltip('Added to deck: {}'.format(deck))

        return deck
Beispiel #17
0
    def eventFilter(self, obj, evt):
        if not(evt.type() == QEvent.Paint and self.save_png):
            return super().eventFilter(obj, evt)

        filename, oldsize = self.save_png
        self.save_png = ()

        size = self._page.contentsSize().toSize()
        image = QImage(size, QImage.Format_ARGB32)
        painter = QPainter(image)
        self.render(painter)
        painter.end()
        success = image.save(filename, "png")
        self.resize(oldsize)
        mw.progress.finish()
        if success:
            showInfo("Image saved to %s!" % os.path.abspath(filename))
        else:
            showCritical("Failed to save the image.")
        return super().eventFilter(obj, evt)
Beispiel #18
0
    def eventFilter(self, obj, evt):
        if not (evt.type() == QEvent.Paint and self.save_png):
            return super().eventFilter(obj, evt)

        filename, oldsize = self.save_png
        self.save_png = ()

        size = self._page.contentsSize().toSize()
        image = QImage(size, QImage.Format_ARGB32)
        painter = QPainter(image)
        self.render(painter)
        painter.end()
        success = image.save(filename, "png")
        self.resize(oldsize)
        mw.progress.finish()
        if success:
            showInfo("Image saved to %s!" % os.path.abspath(filename))
        else:
            showCritical("Failed to save the image.")
        return super().eventFilter(obj, evt)
Beispiel #19
0
    def addCards(self):
        self.editor.saveNow()

        # grab data
        note = self.editor.note

        # sanity check
        if note.dupeOrEmpty():
            showCritical("Note is a dupe or empty; not adding.")
            return

        # check for cloze sanity in case of potential cloze-y notes
        if len(note.fields) == 2:
            # find the highest existing cloze
            highest = note.highestCloze()
            if highest > 0 and not note.isCloze():
                # clozes used, but wrong model, so switch it to cloze
                self.editor.changeToModel("Cloze")
                note = self.editor.note
            elif note.isCloze() and highest == 0:
                # no clozes, switch to basic
                self.editor.changeToModel("Basic")
                note = self.editor.note

        # send data to TCP server in Anki
        data = {
            "model": note.model["name"],
            "deck": self.deckChooser.currentDeck(),
            "fields": note.fields,
            "tags": note.tags,
        }

        ret = sendToAnki("addNote", data)
        if ret == True:
            self.mw.reset()
            # stop anything playing
            clearAudioQueue()
            self.onReset(keep=True)
        else:
            showCritical("Failed to add card. Is Anki ok?")
Beispiel #20
0
    def importWebpage(self, url=None, priority=None, silent=False):
        if not url:
            url, accepted = getText('Enter URL:', title='Import Webpage')
        else:
            accepted = True

        if not url or not accepted:
            return

        if not urlsplit(url).scheme:
            url = 'http://' + url
        elif urlsplit(url).scheme not in ['http', 'https']:
            showCritical('Only HTTP requests are supported.')
            return

        try:
            webpage = self._goose(url)
            body = re.sub("(.+)", r"<p>\1</p>", webpage.cleaned_text)
            title = webpage.title

        except Exception as e:
            showWarning(f'Failed to goose {e}. Falling back to normal processing')
            return

        source = self.settings['sourceFormat'].format(
            date=date.today(), url='<a href="%s">%s</a>' % (url, url)
        )

        if self.settings['prioEnabled'] and not priority:
            priority = self._getPriority(title)

        deck = self._createNote(title, body, source, priority)

        if not silent:
            tooltip('Added to deck: {}'.format(deck))

        return deck
Beispiel #21
0
def errorMsg( msg ):
    showCritical( msg )
    printf( msg )
Beispiel #22
0
def errorMsg(msg):
    showCritical(msg)
    printf(msg)
Beispiel #23
0
def on_run_ocr(browser: Browser):
    time_start = time.time()

    selected_nids = browser.selectedNotes()
    config = mw.addonManager.getConfig(__name__)
    num_notes = len(selected_nids)
    num_batches = ceil(num_notes / config["batch_size"])

    if num_notes == 0:
        showInfo("No cards selected.")
        return
    elif askUser(f"Are you sure you wish to run OCR processing on {num_notes} notes?") is False:
        return

    if config.get("tesseract_install_valid") is not True and config.get("text_output_location") == "new_field":
        showInfo(
            f"Note that because this addon changes the note template, you will see a warning about changing the "
            f"database and uploading to AnkiWeb. \n "
            f"This is normal, and will be shown each time you modify a note template.\n"
            f"This message will be only be shown once.")

    config["tesseract_install_valid"] = True  # Stop the above msg appearing multiple times
    mw.addonManager.writeConfig(__name__, config)

    try:
        progress = mw.progress
        progress.start(immediate=True, min=0, max=num_batches)
        progress.update(value=0, max=num_batches, label="Starting OCR processing...")
    except TypeError:  # old version of Qt/Anki
        progress = None

    ocr = OCR(col=mw.col,
              progress=progress,
              languages=config["languages"],
              text_output_location=config["text_output_location"],
              tesseract_exec_pth=config["tesseract_exec_path"] if config["override_tesseract_exec"] else None,
              batch_size=config["batch_size"], num_threads=config["num_threads"], use_batching=config["use_batching"],
              use_multithreading=config["use_multithreading"])
    try:
        ocr.run_ocr_on_notes(note_ids=selected_nids)
        if progress:
            progress.finish()
        time_taken = time.time() - time_start
        log_messages = logger.handlers[0].flush()
        showInfo(
            f"Processed OCR for {num_notes} notes in {round(time_taken, 1)}s ({round(time_taken / num_notes, 1)}s per note)\n"
            f"{log_messages}")

    except pytesseract.TesseractNotFoundError:
        if progress:
            progress.finish()
        showCritical(text=f"Could not find a valid Tesseract-OCR installation! \n"
                          f"Please visit the addon page in at https://ankiweb.net/shared/info/450181164 for"
                          f" install instructions")

    except (RuntimeError, Exception) as exc:
        from . import __version__ as anki_ocr_version
        from anki.buildinfo import version as anki_version
        import sys
        import platform

        if progress:
            progress.finish()
        msg = f"Error encountered during processing. Debug info: \n" \
              f"Anki Version: {anki_version} , AnkiOCR Version: {anki_ocr_version}\n" \
              f"Platform: {platform.system()} , Python Version: {sys.version}"
        log_messages = logger.handlers[0].flush()
        if len(log_messages) > 0:
            msg += f"Logging message generated during processing:\n{log_messages}"
        exception_str: List[str] = traceback.format_exception(etype=type(exc), value=exc, tb=exc.__traceback__)
        msg += "".join(exception_str)
        showInfo(msg)

    finally:

        browser.model.reset()
        mw.requireReset()
Beispiel #24
0
def create_new_cloze(reviewer, the_card, ease):

    if ease != 1 and \
            len(the_card.sub_questions) != 0 and \
            len(the_card.sub_answers) != 0:

        m = the_card.model()
        try:
            v_major, v_minor = m['ver'].split(".")
            ver_dif = int(TG_MODEL_VERSION['major']) - int(v_major)
            if ver_dif < 0:
                showCritical("""this note works with newer Textograph Version.
                so we can not create new Card.update Textograph Add-on first then try again
                """)
                return
        except (ValueError, NameError, KeyError):
            showCritical(
                "there was an error with this note type. if it happens with newly created notes "
                "please contact add-on author")

        cur_shown_leafs = ""
        cloz_fld_name = TG_FIELDS['cloze']
        txt_cloze_field = the_card.note()[cloz_fld_name]
        match_str = r"(sub_answer\[" + the_card.cloze_id + r"\]\s=\s\[(.*?))\];"
        match = re.search(match_str, txt_cloze_field, re.MULTILINE | re.DOTALL)

        if match:
            cur_shown_leafs = match.group(2)
            sub_cloze = match.group(1)
        else:
            sub_cloze = "sub_answer[1] = ["
            txt_cloze_field = f"""<script id="main">
            {sub_cloze}];
            </script>
            {{{{c1::<script>delete sub_answer[1]</script>}}}}"""

        # remove difficult leafs from current card
        # sub_question is a temp card_obj property that introduced by me for saving interactions

        arr_index, clz_index = get_indices(txt_cloze_field)

        # change this card
        repl_str = sub_cloze + ", ".join(the_card.sub_questions) + ","
        txt_cloze_field = txt_cloze_field.replace(sub_cloze, repl_str)

        # add new card
        match_str = r"<script id=\"main\">(.*?)<\/script>"
        repl_str = r'<script id="main">\1sub_answer[{id}] = [{values},];\n</script>'
        repl_str = repl_str.format(id=arr_index,
                                   values=cur_shown_leafs +
                                   ", ".join(the_card.sub_answers))
        new_cloze = "\n{{{{c{id}::<script>delete sub_answer[{id}]</script>}}}}"
        txt_cloze_field = "{0}{1}".format(
            re.sub(match_str,
                   repl_str,
                   txt_cloze_field,
                   count=0,
                   flags=re.MULTILINE | re.DOTALL),
            new_cloze.format(id=clz_index))

        the_card.note()[cloz_fld_name] = txt_cloze_field
        the_card.note().flush()