mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 05:52:22 -04:00
Drop Pauker and SuperMemo importers from legacy importer
The legacy importer has only been kept around to support some add-ons, and these are so infrequently used that they're better off shifted to add-ons (even they even still work)
This commit is contained in:
parent
9b287dc51a
commit
73edf23954
9 changed files with 2 additions and 611 deletions
2
LICENSE
2
LICENSE
|
@ -6,8 +6,6 @@ The following included source code items use a license other than AGPL3:
|
||||||
|
|
||||||
In the pylib folder:
|
In the pylib folder:
|
||||||
|
|
||||||
* The SuperMemo importer: GPL3 and 0BSD.
|
|
||||||
* The Pauker importer: BSD-3.
|
|
||||||
* statsbg.py: CC BY 4.0.
|
* statsbg.py: CC BY 4.0.
|
||||||
|
|
||||||
In the qt folder:
|
In the qt folder:
|
||||||
|
|
|
@ -65,7 +65,6 @@ importing-with-deck-configs-help =
|
||||||
If enabled, any deck options that the deck sharer included will also be imported.
|
If enabled, any deck options that the deck sharer included will also be imported.
|
||||||
Otherwise, all decks will be assigned the default preset.
|
Otherwise, all decks will be assigned the default preset.
|
||||||
importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip)
|
importing-packaged-anki-deckcollection-apkg-colpkg-zip = Packaged Anki Deck/Collection (*.apkg *.colpkg *.zip)
|
||||||
importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz)
|
|
||||||
# the '|' character
|
# the '|' character
|
||||||
importing-pipe = Pipe
|
importing-pipe = Pipe
|
||||||
# Warning displayed when the csv import preview table is clipped (some columns were hidden)
|
# Warning displayed when the csv import preview table is clipped (some columns were hidden)
|
||||||
|
@ -78,7 +77,6 @@ importing-rows-had-num1d-fields-expected-num2d = '{ $row }' had { $found } field
|
||||||
importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual.
|
importing-selected-file-was-not-in-utf8 = Selected file was not in UTF-8 format. Please see the importing section of the manual.
|
||||||
importing-semicolon = Semicolon
|
importing-semicolon = Semicolon
|
||||||
importing-skipped = Skipped
|
importing-skipped = Skipped
|
||||||
importing-supermemo-xml-export-xml = Supermemo XML export (*.xml)
|
|
||||||
importing-tab = Tab
|
importing-tab = Tab
|
||||||
importing-tag-modified-notes = Tag modified notes:
|
importing-tag-modified-notes = Tag modified notes:
|
||||||
importing-text-separated-by-tabs-or-semicolons = Text separated by tabs or semicolons (*)
|
importing-text-separated-by-tabs-or-semicolons = Text separated by tabs or semicolons (*)
|
||||||
|
@ -252,3 +250,5 @@ importing-importing-collection = Importing collection...
|
||||||
importing-unable-to-import-filename = Unable to import { $filename }: file type not supported
|
importing-unable-to-import-filename = Unable to import { $filename }: file type not supported
|
||||||
importing-notes-that-could-not-be-imported = Notes that could not be imported as note type has changed: { $val }
|
importing-notes-that-could-not-be-imported = Notes that could not be imported as note type has changed: { $val }
|
||||||
importing-added = Added
|
importing-added = Added
|
||||||
|
importing-pauker-18-lesson-paugz = Pauker 1.8 Lesson (*.pau.gz)
|
||||||
|
importing-supermemo-xml-export-xml = Supermemo XML export (*.xml)
|
||||||
|
|
|
@ -11,8 +11,6 @@ from anki.importing.apkg import AnkiPackageImporter
|
||||||
from anki.importing.base import Importer
|
from anki.importing.base import Importer
|
||||||
from anki.importing.csvfile import TextImporter
|
from anki.importing.csvfile import TextImporter
|
||||||
from anki.importing.mnemo import MnemosyneImporter
|
from anki.importing.mnemo import MnemosyneImporter
|
||||||
from anki.importing.pauker import PaukerImporter
|
|
||||||
from anki.importing.supermemo_xml import SupermemoXmlImporter # type: ignore
|
|
||||||
from anki.lang import TR
|
from anki.lang import TR
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,8 +22,6 @@ def importers(col: Collection) -> Sequence[tuple[str, type[Importer]]]:
|
||||||
AnkiPackageImporter,
|
AnkiPackageImporter,
|
||||||
),
|
),
|
||||||
(col.tr.importing_mnemosyne_20_deck_db(), MnemosyneImporter),
|
(col.tr.importing_mnemosyne_20_deck_db(), MnemosyneImporter),
|
||||||
(col.tr.importing_supermemo_xml_export_xml(), SupermemoXmlImporter),
|
|
||||||
(col.tr.importing_pauker_18_lesson_paugz(), PaukerImporter),
|
|
||||||
]
|
]
|
||||||
anki.hooks.importing_importers(importers)
|
anki.hooks.importing_importers(importers)
|
||||||
return importers
|
return importers
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
# Copyright: Andreas Klauer <Andreas.Klauer@metamorpher.de>
|
|
||||||
# License: BSD-3
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
|
|
||||||
import gzip
|
|
||||||
import html
|
|
||||||
import math
|
|
||||||
import random
|
|
||||||
import time
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
|
||||||
from anki.stdmodels import _legacy_add_forward_reverse
|
|
||||||
|
|
||||||
ONE_DAY = 60 * 60 * 24
|
|
||||||
|
|
||||||
|
|
||||||
class PaukerImporter(NoteImporter):
|
|
||||||
"""Import Pauker 1.8 Lesson (*.pau.gz)"""
|
|
||||||
|
|
||||||
needMapper = False
|
|
||||||
allowHTML = True
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
model = _legacy_add_forward_reverse(self.col)
|
|
||||||
model["name"] = "Pauker"
|
|
||||||
self.col.models.save(model, updateReqs=False)
|
|
||||||
self.col.models.set_current(model)
|
|
||||||
self.model = model
|
|
||||||
self.initMapping()
|
|
||||||
NoteImporter.run(self)
|
|
||||||
|
|
||||||
def fields(self):
|
|
||||||
"""Pauker is Front/Back"""
|
|
||||||
return 2
|
|
||||||
|
|
||||||
def foreignNotes(self):
|
|
||||||
"""Build and return a list of notes."""
|
|
||||||
notes = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
f = gzip.open(self.file)
|
|
||||||
tree = ET.parse(f) # type: ignore
|
|
||||||
lesson = tree.getroot()
|
|
||||||
assert lesson.tag == "Lesson"
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
index = -4
|
|
||||||
|
|
||||||
for batch in lesson.findall("./Batch"):
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
for card in batch.findall("./Card"):
|
|
||||||
# Create a note for this card.
|
|
||||||
front = card.findtext("./FrontSide/Text")
|
|
||||||
back = card.findtext("./ReverseSide/Text")
|
|
||||||
note = ForeignNote()
|
|
||||||
assert front and back
|
|
||||||
note.fields = [
|
|
||||||
html.escape(x.strip())
|
|
||||||
.replace("\n", "<br>")
|
|
||||||
.replace(" ", " ")
|
|
||||||
for x in [front, back]
|
|
||||||
]
|
|
||||||
notes.append(note)
|
|
||||||
|
|
||||||
# Determine due date for cards.
|
|
||||||
frontdue = card.find("./FrontSide[@LearnedTimestamp]")
|
|
||||||
backdue = card.find("./ReverseSide[@Batch][@LearnedTimestamp]")
|
|
||||||
|
|
||||||
if frontdue is not None:
|
|
||||||
note.cards[0] = self._learnedCard(
|
|
||||||
index, int(frontdue.attrib["LearnedTimestamp"])
|
|
||||||
)
|
|
||||||
|
|
||||||
if backdue is not None:
|
|
||||||
note.cards[1] = self._learnedCard(
|
|
||||||
int(backdue.attrib["Batch"]),
|
|
||||||
int(backdue.attrib["LearnedTimestamp"]),
|
|
||||||
)
|
|
||||||
|
|
||||||
return notes
|
|
||||||
|
|
||||||
def _learnedCard(self, batch, timestamp):
|
|
||||||
ivl = math.exp(batch)
|
|
||||||
now = time.time()
|
|
||||||
due = ivl - (now - timestamp / 1000.0) / ONE_DAY
|
|
||||||
fc = ForeignCard()
|
|
||||||
fc.due = self.col.sched.today + int(due + 0.5)
|
|
||||||
fc.ivl = random.randint(int(ivl * 0.90), int(ivl + 0.5))
|
|
||||||
fc.factor = random.randint(1500, 2500)
|
|
||||||
return fc
|
|
|
@ -1,484 +0,0 @@
|
||||||
# Copyright: petr.michalec@gmail.com
|
|
||||||
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
# pytype: disable=attribute-error
|
|
||||||
# type: ignore
|
|
||||||
# pylint: disable=C
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import unicodedata
|
|
||||||
from string import capwords
|
|
||||||
from xml.dom import minidom
|
|
||||||
from xml.dom.minidom import Element, Text
|
|
||||||
|
|
||||||
from anki.collection import Collection
|
|
||||||
from anki.importing.noteimp import ForeignCard, ForeignNote, NoteImporter
|
|
||||||
from anki.stdmodels import _legacy_add_basic_model
|
|
||||||
|
|
||||||
|
|
||||||
class SmartDict(dict):
|
|
||||||
"""
|
|
||||||
See http://www.peterbe.com/plog/SmartDict
|
|
||||||
Copyright 2005, Peter Bengtsson, peter@fry-it.com
|
|
||||||
0BSD
|
|
||||||
|
|
||||||
A smart dict can be instantiated either from a pythonic dict
|
|
||||||
or an instance object (eg. SQL recordsets) but it ensures that you can
|
|
||||||
do all the convenient lookups such as x.first_name, x['first_name'] or
|
|
||||||
x.get('first_name').
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *a, **kw) -> None:
|
|
||||||
if a:
|
|
||||||
if isinstance(type(a[0]), dict):
|
|
||||||
kw.update(a[0])
|
|
||||||
elif isinstance(type(a[0]), object):
|
|
||||||
kw.update(a[0].__dict__)
|
|
||||||
elif hasattr(a[0], "__class__") and a[0].__class__.__name__ == "SmartDict":
|
|
||||||
kw.update(a[0].__dict__)
|
|
||||||
|
|
||||||
dict.__init__(self, **kw)
|
|
||||||
self.__dict__ = self
|
|
||||||
|
|
||||||
|
|
||||||
class SuperMemoElement(SmartDict):
|
|
||||||
"SmartDict wrapper to store SM Element data"
|
|
||||||
|
|
||||||
def __init__(self, *a, **kw) -> None:
|
|
||||||
SmartDict.__init__(self, *a, **kw)
|
|
||||||
# default content
|
|
||||||
self.__dict__["lTitle"] = None
|
|
||||||
self.__dict__["Title"] = None
|
|
||||||
self.__dict__["Question"] = None
|
|
||||||
self.__dict__["Answer"] = None
|
|
||||||
self.__dict__["Count"] = None
|
|
||||||
self.__dict__["Type"] = None
|
|
||||||
self.__dict__["ID"] = None
|
|
||||||
self.__dict__["Interval"] = None
|
|
||||||
self.__dict__["Lapses"] = None
|
|
||||||
self.__dict__["Repetitions"] = None
|
|
||||||
self.__dict__["LastRepetiton"] = None
|
|
||||||
self.__dict__["AFactor"] = None
|
|
||||||
self.__dict__["UFactor"] = None
|
|
||||||
|
|
||||||
|
|
||||||
# This is an AnkiImporter
|
|
||||||
class SupermemoXmlImporter(NoteImporter):
|
|
||||||
needMapper = False
|
|
||||||
allowHTML = True
|
|
||||||
|
|
||||||
"""
|
|
||||||
Supermemo XML export's to Anki parser.
|
|
||||||
Goes through a SM collection and fetch all elements.
|
|
||||||
|
|
||||||
My SM collection was a big mess where topics and items were mixed.
|
|
||||||
I was unable to parse my content in a regular way like for loop on
|
|
||||||
minidom.getElementsByTagName() etc. My collection had also an
|
|
||||||
limitation, topics were splited into branches with max 100 items
|
|
||||||
on each. Learning themes were in deep structure. I wanted to have
|
|
||||||
full title on each element to be stored in tags.
|
|
||||||
|
|
||||||
Code should be upgrade to support importing of SM2006 exports.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, col: Collection, file: str) -> None:
|
|
||||||
"""Initialize internal variables.
|
|
||||||
Pameters to be exposed to GUI are stored in self.META"""
|
|
||||||
NoteImporter.__init__(self, col, file)
|
|
||||||
m = _legacy_add_basic_model(self.col)
|
|
||||||
m["name"] = "Supermemo"
|
|
||||||
self.col.models.save(m)
|
|
||||||
self.initMapping()
|
|
||||||
|
|
||||||
self.lines = None
|
|
||||||
self.numFields = int(2)
|
|
||||||
|
|
||||||
# SmXmlParse VARIABLES
|
|
||||||
self.xmldoc = None
|
|
||||||
self.pieces = []
|
|
||||||
self.cntBuf = [] # to store last parsed data
|
|
||||||
self.cntElm = [] # to store SM Elements data
|
|
||||||
self.cntCol = [] # to store SM Colections data
|
|
||||||
|
|
||||||
# store some meta info related to parse algorithm
|
|
||||||
# SmartDict works like dict / class wrapper
|
|
||||||
self.cntMeta = SmartDict()
|
|
||||||
self.cntMeta.popTitles = False
|
|
||||||
self.cntMeta.title = []
|
|
||||||
|
|
||||||
# META stores controls of import script, should be
|
|
||||||
# exposed to import dialog. These are default values.
|
|
||||||
self.META = SmartDict()
|
|
||||||
self.META.resetLearningData = False # implemented
|
|
||||||
self.META.onlyMemorizedItems = False # implemented
|
|
||||||
self.META.loggerLevel = 2 # implemented 0no,1info,2error,3debug
|
|
||||||
self.META.tagAllTopics = True
|
|
||||||
self.META.pathsToBeTagged = [
|
|
||||||
"English for beginners",
|
|
||||||
"Advanced English 97",
|
|
||||||
"Phrasal Verbs",
|
|
||||||
] # path patterns to be tagged - in gui entered like 'Advanced English 97|My Vocablary'
|
|
||||||
self.META.tagMemorizedItems = True # implemented
|
|
||||||
self.META.logToStdOutput = False # implemented
|
|
||||||
|
|
||||||
self.notes = []
|
|
||||||
|
|
||||||
## TOOLS
|
|
||||||
|
|
||||||
def _fudgeText(self, text: str) -> str:
|
|
||||||
"Replace sm syntax to Anki syntax"
|
|
||||||
text = text.replace("\n\r", "<br>")
|
|
||||||
text = text.replace("\n", "<br>")
|
|
||||||
return text
|
|
||||||
|
|
||||||
def _unicode2ascii(self, str: str) -> str:
|
|
||||||
"Remove diacritic punctuation from strings (titles)"
|
|
||||||
return "".join(
|
|
||||||
[
|
|
||||||
c
|
|
||||||
for c in unicodedata.normalize("NFKD", str)
|
|
||||||
if not unicodedata.combining(c)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def _decode_htmlescapes(self, html: str) -> str:
|
|
||||||
"""Unescape HTML code."""
|
|
||||||
# In case of bad formatted html you can import MinimalSoup etc.. see BeautifulSoup source code
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
# my sm2004 also ecaped & char in escaped sequences.
|
|
||||||
html = re.sub("&", "&", html)
|
|
||||||
|
|
||||||
# https://anki.tenderapp.com/discussions/ankidesktop/39543-anki-is-replacing-the-character-by-when-i-exit-the-html-edit-mode-ctrlshiftx
|
|
||||||
if html.find(">") < 0:
|
|
||||||
return html
|
|
||||||
|
|
||||||
# unescaped solitary chars < or > that were ok for minidom confuse btfl soup
|
|
||||||
# html = re.sub(u'>',u'>',html)
|
|
||||||
# html = re.sub(u'<',u'<',html)
|
|
||||||
|
|
||||||
return str(BeautifulSoup(html, "html.parser"))
|
|
||||||
|
|
||||||
def _afactor2efactor(self, af: float) -> float:
|
|
||||||
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
|
|
||||||
|
|
||||||
# Ranges for A-factors and E-factors
|
|
||||||
af_min = 1.2
|
|
||||||
af_max = 6.9
|
|
||||||
ef_min = 1.3
|
|
||||||
ef_max = 3.3
|
|
||||||
|
|
||||||
# Sanity checks for the A-factor
|
|
||||||
if af < af_min:
|
|
||||||
af = af_min
|
|
||||||
elif af > af_max:
|
|
||||||
af = af_max
|
|
||||||
|
|
||||||
# Scale af to the range 0..1
|
|
||||||
af_scaled = (af - af_min) / (af_max - af_min)
|
|
||||||
# Rescale to the interval ef_min..ef_max
|
|
||||||
ef = ef_min + af_scaled * (ef_max - ef_min)
|
|
||||||
|
|
||||||
return ef
|
|
||||||
|
|
||||||
## DEFAULT IMPORTER METHODS
|
|
||||||
|
|
||||||
def foreignNotes(self) -> list[ForeignNote]:
|
|
||||||
# Load file and parse it by minidom
|
|
||||||
self.loadSource(self.file)
|
|
||||||
|
|
||||||
# Migrating content / time consuming part
|
|
||||||
# addItemToCards is called for each sm element
|
|
||||||
self.logger("Parsing started.")
|
|
||||||
self.parse()
|
|
||||||
self.logger("Parsing done.")
|
|
||||||
|
|
||||||
# Return imported cards
|
|
||||||
self.total = len(self.notes)
|
|
||||||
self.log.append("%d cards imported." % self.total)
|
|
||||||
return self.notes
|
|
||||||
|
|
||||||
def fields(self) -> int:
|
|
||||||
return 2
|
|
||||||
|
|
||||||
## PARSER METHODS
|
|
||||||
|
|
||||||
def addItemToCards(self, item: SuperMemoElement) -> None:
|
|
||||||
"This method actually do conversion"
|
|
||||||
|
|
||||||
# new anki card
|
|
||||||
note = ForeignNote()
|
|
||||||
|
|
||||||
# clean Q and A
|
|
||||||
note.fields.append(self._fudgeText(self._decode_htmlescapes(item.Question)))
|
|
||||||
note.fields.append(self._fudgeText(self._decode_htmlescapes(item.Answer)))
|
|
||||||
note.tags = []
|
|
||||||
|
|
||||||
# pre-process scheduling data
|
|
||||||
# convert learning data
|
|
||||||
if (
|
|
||||||
not self.META.resetLearningData
|
|
||||||
and int(item.Interval) >= 1
|
|
||||||
and getattr(item, "LastRepetition", None)
|
|
||||||
):
|
|
||||||
# migration of LearningData algorithm
|
|
||||||
tLastrep = time.mktime(time.strptime(item.LastRepetition, "%d.%m.%Y"))
|
|
||||||
tToday = time.time()
|
|
||||||
card = ForeignCard()
|
|
||||||
card.ivl = int(item.Interval)
|
|
||||||
card.lapses = int(item.Lapses)
|
|
||||||
card.reps = int(item.Repetitions) + int(item.Lapses)
|
|
||||||
nextDue = tLastrep + (float(item.Interval) * 86400.0)
|
|
||||||
remDays = int((nextDue - time.time()) / 86400)
|
|
||||||
card.due = self.col.sched.today + remDays
|
|
||||||
card.factor = int(
|
|
||||||
self._afactor2efactor(float(item.AFactor.replace(",", "."))) * 1000
|
|
||||||
)
|
|
||||||
note.cards[0] = card
|
|
||||||
|
|
||||||
# categories & tags
|
|
||||||
# it's worth to have every theme (tree structure of sm collection) stored in tags, but sometimes not
|
|
||||||
# you can deceide if you are going to tag all toppics or just that containing some pattern
|
|
||||||
tTaggTitle = False
|
|
||||||
for pattern in self.META.pathsToBeTagged:
|
|
||||||
if (
|
|
||||||
item.lTitle is not None
|
|
||||||
and pattern.lower() in " ".join(item.lTitle).lower()
|
|
||||||
):
|
|
||||||
tTaggTitle = True
|
|
||||||
break
|
|
||||||
if tTaggTitle or self.META.tagAllTopics:
|
|
||||||
# normalize - remove diacritic punctuation from unicode chars to ascii
|
|
||||||
item.lTitle = [self._unicode2ascii(topic) for topic in item.lTitle]
|
|
||||||
|
|
||||||
# Transform xyz / aaa / bbb / ccc on Title path to Tag xyzAaaBbbCcc
|
|
||||||
# clean things like [999] or [111-2222] from title path, example: xyz / [1000-1200] zyx / xyz
|
|
||||||
# clean whitespaces
|
|
||||||
# set Capital letters for first char of the word
|
|
||||||
tmp = list(
|
|
||||||
{re.sub(r"(\[[0-9]+\])", " ", i).replace("_", " ") for i in item.lTitle}
|
|
||||||
)
|
|
||||||
tmp = list({re.sub(r"(\W)", " ", i) for i in tmp})
|
|
||||||
tmp = list({re.sub("^[0-9 ]+$", "", i) for i in tmp})
|
|
||||||
tmp = list({capwords(i).replace(" ", "") for i in tmp})
|
|
||||||
tags = [j[0].lower() + j[1:] for j in tmp if j.strip() != ""]
|
|
||||||
|
|
||||||
note.tags += tags
|
|
||||||
|
|
||||||
if self.META.tagMemorizedItems and int(item.Interval) > 0:
|
|
||||||
note.tags.append("Memorized")
|
|
||||||
|
|
||||||
self.logger("Element tags\t- " + repr(note.tags), level=3)
|
|
||||||
|
|
||||||
self.notes.append(note)
|
|
||||||
|
|
||||||
def logger(self, text: str, level: int = 1) -> None:
|
|
||||||
"Wrapper for Anki logger"
|
|
||||||
|
|
||||||
dLevels = {0: "", 1: "Info", 2: "Verbose", 3: "Debug"}
|
|
||||||
if level <= self.META.loggerLevel:
|
|
||||||
# self.deck.updateProgress(_(text))
|
|
||||||
|
|
||||||
if self.META.logToStdOutput:
|
|
||||||
print(
|
|
||||||
self.__class__.__name__
|
|
||||||
+ " - "
|
|
||||||
+ dLevels[level].ljust(9)
|
|
||||||
+ " -\t"
|
|
||||||
+ text
|
|
||||||
)
|
|
||||||
|
|
||||||
# OPEN AND LOAD
|
|
||||||
def openAnything(self, source):
|
|
||||||
"""Open any source / actually only opening of files is used
|
|
||||||
@return an open handle which must be closed after use, i.e., handle.close()"""
|
|
||||||
|
|
||||||
if source == "-":
|
|
||||||
return sys.stdin
|
|
||||||
|
|
||||||
# try to open with urllib (if source is http, ftp, or file URL)
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
try:
|
|
||||||
return urllib.request.urlopen(source)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# try to open with native open function (if source is pathname)
|
|
||||||
try:
|
|
||||||
return open(source, encoding="utf8")
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# treat source as string
|
|
||||||
import io
|
|
||||||
|
|
||||||
return io.StringIO(str(source))
|
|
||||||
|
|
||||||
def loadSource(self, source: str) -> None:
|
|
||||||
"""Load source file and parse with xml.dom.minidom"""
|
|
||||||
self.source = source
|
|
||||||
self.logger("Load started...")
|
|
||||||
sock = open(self.source, encoding="utf8")
|
|
||||||
self.xmldoc = minidom.parse(sock).documentElement
|
|
||||||
sock.close()
|
|
||||||
self.logger("Load done.")
|
|
||||||
|
|
||||||
# PARSE
|
|
||||||
def parse(self, node: Text | Element | None = None) -> None:
|
|
||||||
"Parse method - parses document elements"
|
|
||||||
|
|
||||||
if node is None and self.xmldoc is not None:
|
|
||||||
node = self.xmldoc
|
|
||||||
|
|
||||||
_method = "parse_%s" % node.__class__.__name__
|
|
||||||
if hasattr(self, _method):
|
|
||||||
parseMethod = getattr(self, _method)
|
|
||||||
parseMethod(node)
|
|
||||||
else:
|
|
||||||
self.logger("No handler for method %s" % _method, level=3)
|
|
||||||
|
|
||||||
def parse_Document(self, node):
|
|
||||||
"Parse XML document"
|
|
||||||
|
|
||||||
self.parse(node.documentElement)
|
|
||||||
|
|
||||||
def parse_Element(self, node: Element) -> None:
|
|
||||||
"Parse XML element"
|
|
||||||
|
|
||||||
_method = "do_%s" % node.tagName
|
|
||||||
if hasattr(self, _method):
|
|
||||||
handlerMethod = getattr(self, _method)
|
|
||||||
handlerMethod(node)
|
|
||||||
else:
|
|
||||||
self.logger("No handler for method %s" % _method, level=3)
|
|
||||||
# print traceback.print_exc()
|
|
||||||
|
|
||||||
def parse_Text(self, node: Text) -> None:
|
|
||||||
"Parse text inside elements. Text is stored into local buffer."
|
|
||||||
|
|
||||||
text = node.data
|
|
||||||
self.cntBuf.append(text)
|
|
||||||
|
|
||||||
# def parse_Comment(self, node):
|
|
||||||
# """
|
|
||||||
# Source can contain XML comments, but we ignore them
|
|
||||||
# """
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# DO
|
|
||||||
def do_SuperMemoCollection(self, node: Element) -> None:
|
|
||||||
"Process SM Collection"
|
|
||||||
|
|
||||||
for child in node.childNodes:
|
|
||||||
self.parse(child)
|
|
||||||
|
|
||||||
def do_SuperMemoElement(self, node: Element) -> None:
|
|
||||||
"Process SM Element (Type - Title,Topics)"
|
|
||||||
|
|
||||||
self.logger("=" * 45, level=3)
|
|
||||||
|
|
||||||
self.cntElm.append(SuperMemoElement())
|
|
||||||
self.cntElm[-1]["lTitle"] = self.cntMeta["title"]
|
|
||||||
|
|
||||||
# parse all child elements
|
|
||||||
for child in node.childNodes:
|
|
||||||
self.parse(child)
|
|
||||||
|
|
||||||
# strip all saved strings, just for sure
|
|
||||||
for key in list(self.cntElm[-1].keys()):
|
|
||||||
if hasattr(self.cntElm[-1][key], "strip"):
|
|
||||||
self.cntElm[-1][key] = self.cntElm[-1][key].strip()
|
|
||||||
|
|
||||||
# pop current element
|
|
||||||
smel = self.cntElm.pop()
|
|
||||||
|
|
||||||
# Process cntElm if is valid Item (and not an Topic etc..)
|
|
||||||
# if smel.Lapses != None and smel.Interval != None and smel.Question != None and smel.Answer != None:
|
|
||||||
if smel.Title is None and smel.Question is not None and smel.Answer is not None:
|
|
||||||
if smel.Answer.strip() != "" and smel.Question.strip() != "":
|
|
||||||
# migrate only memorized otherway skip/continue
|
|
||||||
if self.META.onlyMemorizedItems and not (int(smel.Interval) > 0):
|
|
||||||
self.logger("Element skipped \t- not memorized ...", level=3)
|
|
||||||
else:
|
|
||||||
# import sm element data to Anki
|
|
||||||
self.addItemToCards(smel)
|
|
||||||
self.logger("Import element \t- " + smel["Question"], level=3)
|
|
||||||
|
|
||||||
# print element
|
|
||||||
self.logger("-" * 45, level=3)
|
|
||||||
for key in list(smel.keys()):
|
|
||||||
self.logger(
|
|
||||||
"\t{} {}".format((key + ":").ljust(15), smel[key]), level=3
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.logger("Element skipped \t- no valid Q and A ...", level=3)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# now we know that item was topic
|
|
||||||
# parsing of whole node is now finished
|
|
||||||
|
|
||||||
# test if it's really topic
|
|
||||||
if smel.Title is not None:
|
|
||||||
# remove topic from title list
|
|
||||||
t = self.cntMeta["title"].pop()
|
|
||||||
self.logger("End of topic \t- %s" % (t), level=2)
|
|
||||||
|
|
||||||
def do_Content(self, node: Element) -> None:
|
|
||||||
"Process SM element Content"
|
|
||||||
|
|
||||||
for child in node.childNodes:
|
|
||||||
if hasattr(child, "tagName") and child.firstChild is not None:
|
|
||||||
self.cntElm[-1][child.tagName] = child.firstChild.data
|
|
||||||
|
|
||||||
def do_LearningData(self, node: Element) -> None:
|
|
||||||
"Process SM element LearningData"
|
|
||||||
|
|
||||||
for child in node.childNodes:
|
|
||||||
if hasattr(child, "tagName") and child.firstChild is not None:
|
|
||||||
self.cntElm[-1][child.tagName] = child.firstChild.data
|
|
||||||
|
|
||||||
# It's being processed in do_Content now
|
|
||||||
# def do_Question(self, node):
|
|
||||||
# for child in node.childNodes: self.parse(child)
|
|
||||||
# self.cntElm[-1][node.tagName]=self.cntBuf.pop()
|
|
||||||
|
|
||||||
# It's being processed in do_Content now
|
|
||||||
# def do_Answer(self, node):
|
|
||||||
# for child in node.childNodes: self.parse(child)
|
|
||||||
# self.cntElm[-1][node.tagName]=self.cntBuf.pop()
|
|
||||||
|
|
||||||
def do_Title(self, node: Element) -> None:
|
|
||||||
"Process SM element Title"
|
|
||||||
|
|
||||||
t = self._decode_htmlescapes(node.firstChild.data)
|
|
||||||
self.cntElm[-1][node.tagName] = t
|
|
||||||
self.cntMeta["title"].append(t)
|
|
||||||
self.cntElm[-1]["lTitle"] = self.cntMeta["title"]
|
|
||||||
self.logger("Start of topic \t- " + " / ".join(self.cntMeta["title"]), level=2)
|
|
||||||
|
|
||||||
def do_Type(self, node: Element) -> None:
|
|
||||||
"Process SM element Type"
|
|
||||||
|
|
||||||
if len(self.cntBuf) >= 1:
|
|
||||||
self.cntElm[-1][node.tagName] = self.cntBuf.pop()
|
|
||||||
|
|
||||||
|
|
||||||
# if __name__ == '__main__':
|
|
||||||
|
|
||||||
# for testing you can start it standalone
|
|
||||||
|
|
||||||
# file = u'/home/epcim/hg2g/dev/python/sm2anki/ADVENG2EXP.xxe.esc.zaloha_FINAL.xml'
|
|
||||||
# file = u'/home/epcim/hg2g/dev/python/anki/libanki/tests/importing/supermemo/original_ENGLISHFORBEGGINERS_noOEM.xml'
|
|
||||||
# file = u'/home/epcim/hg2g/dev/python/anki/libanki/tests/importing/supermemo/original_ENGLISHFORBEGGINERS_oem_1250.xml'
|
|
||||||
# file = str(sys.argv[1])
|
|
||||||
# impo = SupermemoXmlImporter(Deck(),file)
|
|
||||||
# impo.foreignCards()
|
|
||||||
|
|
||||||
# sys.exit(1)
|
|
||||||
|
|
||||||
# vim: ts=4 sts=2 ft=python
|
|
|
@ -4,7 +4,6 @@ dynamic = ["version"]
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"beautifulsoup4",
|
|
||||||
"decorator",
|
"decorator",
|
||||||
"markdown",
|
"markdown",
|
||||||
"orjson",
|
"orjson",
|
||||||
|
|
|
@ -13,7 +13,6 @@ from anki.importing import (
|
||||||
Anki2Importer,
|
Anki2Importer,
|
||||||
AnkiPackageImporter,
|
AnkiPackageImporter,
|
||||||
MnemosyneImporter,
|
MnemosyneImporter,
|
||||||
SupermemoXmlImporter,
|
|
||||||
TextImporter,
|
TextImporter,
|
||||||
)
|
)
|
||||||
from tests.shared import getEmptyCol, getUpgradeDeckPath
|
from tests.shared import getEmptyCol, getUpgradeDeckPath
|
||||||
|
@ -306,22 +305,6 @@ def test_csv_tag_only_if_modified():
|
||||||
col.close()
|
col.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:Using or importing the ABCs")
|
|
||||||
def test_supermemo_xml_01_unicode():
|
|
||||||
col = getEmptyCol()
|
|
||||||
file = str(os.path.join(testDir, "support", "supermemo1.xml"))
|
|
||||||
i = SupermemoXmlImporter(col, file)
|
|
||||||
# i.META.logToStdOutput = True
|
|
||||||
i.run()
|
|
||||||
assert i.total == 1
|
|
||||||
cid = col.db.scalar("select id from cards")
|
|
||||||
c = col.get_card(cid)
|
|
||||||
# Applies A Factor-to-E Factor conversion
|
|
||||||
assert c.factor == 2879
|
|
||||||
assert c.reps == 7
|
|
||||||
col.close()
|
|
||||||
|
|
||||||
|
|
||||||
def test_mnemo():
|
def test_mnemo():
|
||||||
col = getEmptyCol()
|
col = getEmptyCol()
|
||||||
file = str(os.path.join(testDir, "support", "mnemo.db"))
|
file = str(os.path.join(testDir, "support", "mnemo.db"))
|
||||||
|
|
|
@ -21,12 +21,7 @@ use walkdir::WalkDir;
|
||||||
|
|
||||||
const NONSTANDARD_HEADER: &[&str] = &[
|
const NONSTANDARD_HEADER: &[&str] = &[
|
||||||
"./pylib/anki/_vendor/stringcase.py",
|
"./pylib/anki/_vendor/stringcase.py",
|
||||||
"./pylib/anki/importing/pauker.py",
|
|
||||||
"./pylib/anki/importing/supermemo_xml.py",
|
|
||||||
"./pylib/anki/statsbg.py",
|
"./pylib/anki/statsbg.py",
|
||||||
"./pylib/tools/protoc-gen-mypy.py",
|
|
||||||
"./python/pyqt/install.py",
|
|
||||||
"./python/write_wheel.py",
|
|
||||||
"./qt/aqt/mpv.py",
|
"./qt/aqt/mpv.py",
|
||||||
"./qt/aqt/winpaths.py",
|
"./qt/aqt/winpaths.py",
|
||||||
];
|
];
|
||||||
|
|
2
uv.lock
2
uv.lock
|
@ -51,7 +51,6 @@ wheels = [
|
||||||
name = "anki"
|
name = "anki"
|
||||||
source = { editable = "pylib" }
|
source = { editable = "pylib" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "beautifulsoup4" },
|
|
||||||
{ name = "decorator" },
|
{ name = "decorator" },
|
||||||
{ name = "distro", marker = "(sys_platform != 'darwin' and sys_platform != 'win32') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" },
|
{ name = "distro", marker = "(sys_platform != 'darwin' and sys_platform != 'win32') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'darwin' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt66') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt67') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt66' and extra == 'extra-3-aqt-qt69') or (sys_platform == 'win32' and extra == 'extra-3-aqt-qt67' and extra == 'extra-3-aqt-qt69')" },
|
||||||
{ name = "markdown" },
|
{ name = "markdown" },
|
||||||
|
@ -64,7 +63,6 @@ dependencies = [
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "beautifulsoup4" },
|
|
||||||
{ name = "decorator" },
|
{ name = "decorator" },
|
||||||
{ name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" },
|
{ name = "distro", marker = "sys_platform != 'darwin' and sys_platform != 'win32'" },
|
||||||
{ name = "markdown" },
|
{ name = "markdown" },
|
||||||
|
|
Loading…
Reference in a new issue