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 import sys
from urllib.error import URLError from urllib.error import URLError
from antp.exporter import export_note_type from .common import ANTPError
from antp.importer import import_note_type from .exporter import export_note_type
from .importer import import_note_type
def main(): def main():
@ -11,15 +15,13 @@ def main():
print("No action provided.\n") print("No action provided.\n")
print(f"import\tAdd one of the available note types to Anki.") print(f"import\tAdd one of the available note types to Anki.")
print(f"export\tSave your note type as a template.") print(f"export\tSave your note type as a template.")
return elif (cmd := sys.argv[1]) == 'export':
cmd = sys.argv[1]
if cmd == 'export':
export_note_type() export_note_type()
elif cmd == 'import': elif cmd == 'import':
import_note_type() import_note_type()
except URLError: except URLError:
print("Couldn't connect. Make sure Anki is open and AnkiConnect is installed.") print("Couldn't connect. Make sure Anki is open and AnkiConnect is installed.")
except Exception as ex: except ANTPError as ex:
print(ex) print(ex)

View file

@ -1,8 +1,14 @@
# Taken from https://github.com/FooSoft/anki-connect # 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 json
import urllib.request import urllib.request
from .common import ANTPError
def request(action, **params): def request(action, **params):
return {'action': action, 'params': params, 'version': 6} 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') request_json = json.dumps(request(action, **params)).encode('utf-8')
response = json.load(urllib.request.urlopen(urllib.request.Request('http://localhost:8765', request_json))) response = json.load(urllib.request.urlopen(urllib.request.Request('http://localhost:8765', request_json)))
if len(response) != 2: 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: 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: 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: if response['error'] is not None:
raise Exception(response['error']) raise ANTPError(response['error'])
return response['result'] 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 import re
from dataclasses import dataclass
from typing import Optional from typing import Optional
JSON_INDENT = 4 from .consts import *
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')
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: def read_num(msg: str = "Input number: ", min_val: int = 0, max_val: int = None) -> int:
resp = int(input(msg)) resp = int(input(msg))
if resp < min_val or (max_val and resp > max_val): 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 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): def get_used_fonts(template_css: str):
return re.findall(r"url\([\"'](\w+\.[ot]tf)[\"']\)", template_css, re.IGNORECASE) 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/ # 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 base64
import json import json
import os
import random import random
from typing import AnyStr from typing import Any
from urllib.error import URLError
import antp.common as ac from .ankiconnect import invoke
from antp.ankiconnect import invoke from .common import NoteType, CardTemplate, get_used_fonts, select
from .consts import *
def fetch_card_templates(model_name: str): def fetch_card_templates(model_name: str) -> list[CardTemplate]:
card_templates = [] return [
for key, val in invoke("modelTemplates", modelName=model_name).items(): CardTemplate(name, val["Front"], val["Back"])
card_templates.append({ for name, val in invoke("modelTemplates", modelName=model_name).items()
"Name": key, ]
"Front": val["Front"],
"Back": val["Back"],
})
return card_templates
def fetch_template(model_name: str): def fetch_template(model_name: str) -> NoteType:
return { return NoteType(
"modelName": model_name, name=model_name,
"inOrderFields": invoke("modelFieldNames", modelName=model_name), fields=invoke("modelFieldNames", modelName=model_name),
"css": invoke("modelStyling", modelName=model_name)["css"], css=invoke("modelStyling", modelName=model_name)["css"],
"cardTemplates": fetch_card_templates(model_name), templates=fetch_card_templates(model_name),
} )
def create_template_dir(template_name: str) -> AnyStr: def select_model_dir_path(model_name: str) -> str:
dir_path = os.path.join(ac.NOTE_TYPES_DIR, template_name) dir_path = os.path.join(NOTE_TYPES_DIR, model_name)
dir_content = os.listdir(ac.NOTE_TYPES_DIR) 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]? ") ans = input("Template with this name already exists. Overwrite [y/N]? ")
if ans.lower() != 'y': if ans.lower() != 'y':
while os.path.basename(dir_path) in dir_content: 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)}") dir_path = os.path.join(NOTE_TYPES_DIR, f"{model_name}_{random.randint(0, 9999)}")
if not os.path.isdir(dir_path):
os.mkdir(dir_path)
return dir_path return dir_path
def save_note_type(template_json: dict): def write_card_templates(template_dir: str, templates: list[CardTemplate]) -> None:
template_dir = create_template_dir(template_json["modelName"]) for template in templates:
template_fp = os.path.join(template_dir, ac.JSON_FILENAME) dir_path = os.path.join(template_dir, template.name)
readme_fp = os.path.join(template_dir, ac.README_FILENAME) if not os.path.isdir(dir_path):
os.mkdir(dir_path)
with open(template_fp, 'w') as f: for filename, content in zip((FRONT_FILENAME, BACK_FILENAME), (template.front, template.back)):
f.write(json.dumps(template_json, indent=ac.JSON_INDENT)) with open(os.path.join(dir_path, filename), 'w') as f:
f.write(content)
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 save_fonts(template_json: dict): def format_export(model: NoteType) -> dict[str, Any]:
linked_fonts = ac.get_used_fonts(template_json['css']) 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: for font in linked_fonts:
file_b64 = invoke("retrieveMediaFile", filename=font) if file_b64 := invoke("retrieveMediaFile", filename=font):
if file_b64 is False: with open(os.path.join(FONTS_DIR, font), 'bw') as f:
continue
with open(os.path.join(ac.FONTS_DIR, font), 'bw') as f:
f.write(base64.b64decode(file_b64)) f.write(base64.b64decode(file_b64))
def export_note_type(): def export_note_type():
model = ac.select(invoke('modelNames')) if model := select(invoke('modelNames')):
if not model:
return
print(f"Selected model: {model}") print(f"Selected model: {model}")
template = fetch_template(model) template = fetch_template(model)
save_fonts(template) save_fonts(template)
save_note_type(template) save_note_type(template)
print("Done.") 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)

View file

@ -1,46 +1,73 @@
# Importer imports note types from ../templates/ to Anki. # 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 json
import os from typing import Any
from urllib.error import URLError
import antp.common as ac from .ankiconnect import invoke
from antp.ankiconnect import invoke from .common import select, get_used_fonts, NoteType, CardTemplate
from .consts import *
def read_template(model_name: str): def read_css(model_name: str) -> str:
with open(os.path.join(ac.NOTE_TYPES_DIR, model_name, ac.JSON_FILENAME), 'r') as f: with open(os.path.join(NOTE_TYPES_DIR, model_name, CSS_FILENAME)) as f:
return json.load(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') models = invoke('modelNames')
template_json = format_import(model)
while template_json["modelName"] in models: while template_json["modelName"] in models:
template_json["modelName"] = input("Model with this name already exists. Enter new name: ") template_json["modelName"] = input("Model with this name already exists. Enter new name: ")
invoke("createModel", **template_json) invoke("createModel", **template_json)
def store_fonts(fonts: list[str]): 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: 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(): def import_note_type():
model = ac.select(os.listdir(ac.NOTE_TYPES_DIR)) if model_name := select(os.listdir(NOTE_TYPES_DIR)):
if not model: print(f"Selected model: {model_name}")
return with open(os.path.join(NOTE_TYPES_DIR, model_name, JSON_FILENAME), 'r') as f:
print(f"Selected model: {model}") model = read_model(json.load(f))
template = read_template(model) store_fonts(get_used_fonts(model.css))
store_fonts(ac.get_used_fonts(template['css'])) send_note_type(model)
send_note_type(template)
print("Done.") 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)

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()