store html and css in editable format

This commit is contained in:
Ren Tatsumoto 2021-11-27 14:54:42 +03:00
parent 7d67ede1ae
commit 66c91e45dc
10 changed files with 213 additions and 121 deletions

0
antp/__init__.py Normal file
View file

View file

@ -1,8 +1,12 @@
# Copyright: Ren Tatsumoto <tatsu at autistici.org>
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import sys
from urllib.error import URLError
from antp.exporter import export_note_type
from antp.importer import import_note_type
from .common import ANTPError
from .exporter import export_note_type
from .importer import import_note_type
def main():
@ -11,15 +15,13 @@ def main():
print("No action provided.\n")
print(f"import\tAdd one of the available note types to Anki.")
print(f"export\tSave your note type as a template.")
return
cmd = sys.argv[1]
if cmd == 'export':
elif (cmd := sys.argv[1]) == 'export':
export_note_type()
elif cmd == 'import':
import_note_type()
except URLError:
print("Couldn't connect. Make sure Anki is open and AnkiConnect is installed.")
except Exception as ex:
except ANTPError as ex:
print(ex)

View file

@ -1,8 +1,14 @@
# Taken from https://github.com/FooSoft/anki-connect
# Copyright: Ren Tatsumoto <tatsu at autistici.org>
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
__all__ = ['invoke']
import json
import urllib.request
from .common import ANTPError
def request(action, **params):
return {'action': action, 'params': params, 'version': 6}
@ -12,22 +18,11 @@ def invoke(action, **params):
request_json = json.dumps(request(action, **params)).encode('utf-8')
response = json.load(urllib.request.urlopen(urllib.request.Request('http://localhost:8765', request_json)))
if len(response) != 2:
raise Exception('response has an unexpected number of fields')
raise ANTPError('response has an unexpected number of fields')
if 'error' not in response:
raise Exception('response is missing required error field')
raise ANTPError('response is missing required error field')
if 'result' not in response:
raise Exception('response is missing required result field')
raise ANTPError('response is missing required result field')
if response['error'] is not None:
raise Exception(response['error'])
raise ANTPError(response['error'])
return response['result']
def main():
decks = invoke('deckNames')
print("List of decks:")
for deck in decks:
print(deck)
if __name__ == '__main__':
main()

View file

@ -1,22 +1,36 @@
import os
# Copyright: Ren Tatsumoto <tatsu at autistici.org>
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import re
from dataclasses import dataclass
from typing import Optional
JSON_INDENT = 4
JSON_FILENAME = 'template.json'
README_FILENAME = 'README.md'
SCRIPT_DIR = os.path.dirname(__file__)
NOTE_TYPES_DIR = os.path.join(SCRIPT_DIR, os.pardir, 'templates')
FONTS_DIR = os.path.join(SCRIPT_DIR, os.pardir, 'fonts')
from .consts import *
if not os.path.isdir(FONTS_DIR):
os.mkdir(FONTS_DIR)
class ANTPError(Exception):
pass
@dataclass(frozen=True)
class CardTemplate:
name: str
front: str
back: str
@dataclass(frozen=True)
class NoteType:
name: str
fields: list[str]
css: str
templates: list[CardTemplate]
def read_num(msg: str = "Input number: ", min_val: int = 0, max_val: int = None) -> int:
resp = int(input(msg))
if resp < min_val or (max_val and resp > max_val):
raise ValueError("Value out of range.")
raise ANTPError("Value out of range.")
return resp
@ -35,3 +49,14 @@ def select(items: list[str], msg: str = "Select item number: ") -> Optional[str]
def get_used_fonts(template_css: str):
return re.findall(r"url\([\"'](\w+\.[ot]tf)[\"']\)", template_css, re.IGNORECASE)
def init():
from .consts import NOTE_TYPES_DIR, FONTS_DIR
for path in (NOTE_TYPES_DIR, FONTS_DIR):
if not os.path.isdir(path):
os.mkdir(path)
init()

14
antp/consts.py Normal file
View file

@ -0,0 +1,14 @@
# Copyright: Ren Tatsumoto <tatsu at autistici.org>
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import os
JSON_INDENT = 4
JSON_FILENAME = 'template.json'
CSS_FILENAME = 'template.css'
FRONT_FILENAME = 'front.html'
BACK_FILENAME = 'back.html'
README_FILENAME = 'README.md'
SCRIPT_DIR = os.path.dirname(__file__)
NOTE_TYPES_DIR = os.path.join(SCRIPT_DIR, os.pardir, 'templates')
FONTS_DIR = os.path.join(SCRIPT_DIR, os.pardir, 'fonts')

View file

@ -1,89 +1,98 @@
# Exporter exports note types from Anki to ../templates/
# Copyright: Ren Tatsumoto <tatsu at autistici.org>
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import base64
import json
import os
import random
from typing import AnyStr
from urllib.error import URLError
from typing import Any
import antp.common as ac
from antp.ankiconnect import invoke
from .ankiconnect import invoke
from .common import NoteType, CardTemplate, get_used_fonts, select
from .consts import *
def fetch_card_templates(model_name: str):
card_templates = []
for key, val in invoke("modelTemplates", modelName=model_name).items():
card_templates.append({
"Name": key,
"Front": val["Front"],
"Back": val["Back"],
})
return card_templates
def fetch_card_templates(model_name: str) -> list[CardTemplate]:
return [
CardTemplate(name, val["Front"], val["Back"])
for name, val in invoke("modelTemplates", modelName=model_name).items()
]
def fetch_template(model_name: str):
return {
"modelName": model_name,
"inOrderFields": invoke("modelFieldNames", modelName=model_name),
"css": invoke("modelStyling", modelName=model_name)["css"],
"cardTemplates": fetch_card_templates(model_name),
}
def fetch_template(model_name: str) -> NoteType:
return NoteType(
name=model_name,
fields=invoke("modelFieldNames", modelName=model_name),
css=invoke("modelStyling", modelName=model_name)["css"],
templates=fetch_card_templates(model_name),
)
def create_template_dir(template_name: str) -> AnyStr:
dir_path = os.path.join(ac.NOTE_TYPES_DIR, template_name)
dir_content = os.listdir(ac.NOTE_TYPES_DIR)
def select_model_dir_path(model_name: str) -> str:
dir_path = os.path.join(NOTE_TYPES_DIR, model_name)
dir_content = os.listdir(NOTE_TYPES_DIR)
if template_name in dir_content:
if model_name in dir_content:
ans = input("Template with this name already exists. Overwrite [y/N]? ")
if ans.lower() != 'y':
while os.path.basename(dir_path) in dir_content:
dir_path = os.path.join(ac.NOTE_TYPES_DIR, f"{template_name}_{random.randint(0, 9999)}")
if not os.path.isdir(dir_path):
os.mkdir(dir_path)
dir_path = os.path.join(NOTE_TYPES_DIR, f"{model_name}_{random.randint(0, 9999)}")
return dir_path
def save_note_type(template_json: dict):
template_dir = create_template_dir(template_json["modelName"])
template_fp = os.path.join(template_dir, ac.JSON_FILENAME)
readme_fp = os.path.join(template_dir, ac.README_FILENAME)
with open(template_fp, 'w') as f:
f.write(json.dumps(template_json, indent=ac.JSON_INDENT))
if not os.path.isfile(readme_fp):
with open(readme_fp, 'w') as f:
f.write(f"# {template_json['modelName']}\n\n*Description and screenshots here.*")
def write_card_templates(template_dir: str, templates: list[CardTemplate]) -> None:
for template in templates:
dir_path = os.path.join(template_dir, template.name)
if not os.path.isdir(dir_path):
os.mkdir(dir_path)
for filename, content in zip((FRONT_FILENAME, BACK_FILENAME), (template.front, template.back)):
with open(os.path.join(dir_path, filename), 'w') as f:
f.write(content)
def save_fonts(template_json: dict):
linked_fonts = ac.get_used_fonts(template_json['css'])
def format_export(model: NoteType) -> dict[str, Any]:
return {
"modelName": model.name,
"inOrderFields": model.fields,
"cardTemplates": [template.name for template in model.templates]
}
def save_note_type(model: NoteType):
dir_path = select_model_dir_path(model.name)
json_path = os.path.join(dir_path, JSON_FILENAME)
css_path = os.path.join(dir_path, CSS_FILENAME)
readme_path = os.path.join(dir_path, README_FILENAME)
if not os.path.isdir(dir_path):
os.mkdir(dir_path)
with open(json_path, 'w') as f:
json.dump(format_export(model), f, indent=JSON_INDENT)
with open(css_path, 'w') as f:
f.write(model.css)
write_card_templates(dir_path, model.templates)
if not os.path.isfile(readme_path):
with open(readme_path, 'w') as f:
f.write(f"# {model.name}\n\n*Description and screenshots here.*")
def save_fonts(model: NoteType) -> None:
linked_fonts = get_used_fonts(model.css)
for font in linked_fonts:
file_b64 = invoke("retrieveMediaFile", filename=font)
if file_b64 is False:
continue
with open(os.path.join(ac.FONTS_DIR, font), 'bw') as f:
f.write(base64.b64decode(file_b64))
if file_b64 := invoke("retrieveMediaFile", filename=font):
with open(os.path.join(FONTS_DIR, font), 'bw') as f:
f.write(base64.b64decode(file_b64))
def export_note_type():
model = ac.select(invoke('modelNames'))
if not model:
return
print(f"Selected model: {model}")
template = fetch_template(model)
save_fonts(template)
save_note_type(template)
print("Done.")
if __name__ == '__main__':
try:
export_note_type()
except URLError:
print("Couldn't connect. Make sure Anki is open and AnkiConnect is installed.")
except Exception as ex:
print(ex)
if model := select(invoke('modelNames')):
print(f"Selected model: {model}")
template = fetch_template(model)
save_fonts(template)
save_note_type(template)
print("Done.")

View file

@ -1,46 +1,73 @@
# Importer imports note types from ../templates/ to Anki.
# Copyright: Ren Tatsumoto <tatsu at autistici.org>
# License: GNU GPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import json
import os
from urllib.error import URLError
from typing import Any
import antp.common as ac
from antp.ankiconnect import invoke
from .ankiconnect import invoke
from .common import select, get_used_fonts, NoteType, CardTemplate
from .consts import *
def read_template(model_name: str):
with open(os.path.join(ac.NOTE_TYPES_DIR, model_name, ac.JSON_FILENAME), 'r') as f:
return json.load(f)
def read_css(model_name: str) -> str:
with open(os.path.join(NOTE_TYPES_DIR, model_name, CSS_FILENAME)) as f:
return f.read()
def send_note_type(template_json: dict):
def read_card_templates(model_name, template_names: list[str]) -> list[CardTemplate]:
templates = []
for template_name in template_names:
dir_path = os.path.join(NOTE_TYPES_DIR, model_name, template_name)
with open(os.path.join(dir_path, FRONT_FILENAME)) as front, open(os.path.join(dir_path, BACK_FILENAME)) as back:
templates.append(CardTemplate(template_name, front.read(), back.read()))
return templates
def read_model(model_dict: dict[str, Any]) -> NoteType:
return NoteType(
name=model_dict['modelName'],
fields=model_dict['inOrderFields'],
css=read_css(model_dict['modelName']),
templates=read_card_templates(model_dict['modelName'], model_dict['cardTemplates']),
)
def format_import(model: NoteType) -> dict[str, Any]:
return {
"modelName": model.name,
"inOrderFields": model.fields,
"css": model.css,
"cardTemplates": [
{
"Name": template.name,
"Front": template.front,
"Back": template.back,
}
for template in model.templates
]
}
def send_note_type(model: NoteType):
models = invoke('modelNames')
template_json = format_import(model)
while template_json["modelName"] in models:
template_json["modelName"] = input("Model with this name already exists. Enter new name: ")
invoke("createModel", **template_json)
def store_fonts(fonts: list[str]):
for file in os.listdir(ac.FONTS_DIR):
for file in os.listdir(FONTS_DIR):
if file in fonts:
invoke("storeMediaFile", filename=file, path=os.path.join(ac.FONTS_DIR, file))
invoke("storeMediaFile", filename=file, path=os.path.join(FONTS_DIR, file))
def import_note_type():
model = ac.select(os.listdir(ac.NOTE_TYPES_DIR))
if not model:
return
print(f"Selected model: {model}")
template = read_template(model)
store_fonts(ac.get_used_fonts(template['css']))
send_note_type(template)
print("Done.")
if __name__ == '__main__':
try:
import_note_type()
except URLError:
print("Couldn't connect. Make sure Anki is open and AnkiConnect is installed.")
except Exception as ex:
print(ex)
if model_name := select(os.listdir(NOTE_TYPES_DIR)):
print(f"Selected model: {model_name}")
with open(os.path.join(NOTE_TYPES_DIR, model_name, JSON_FILENAME), 'r') as f:
model = read_model(json.load(f))
store_fonts(get_used_fonts(model.css))
send_note_type(model)
print("Done.")

4
tests/export.py Normal file
View file

@ -0,0 +1,4 @@
from antp.exporter import export_note_type
if __name__ == '__main__':
export_note_type()

4
tests/import.py Normal file
View file

@ -0,0 +1,4 @@
from antp.importer import import_note_type
if __name__ == '__main__':
import_note_type()

12
tests/list_decks.py Normal file
View file

@ -0,0 +1,12 @@
from antp.ankiconnect import invoke
def list_decks():
decks = invoke('deckNames')
print("List of decks:")
for deck in decks:
print(deck)
if __name__ == '__main__':
list_decks()