store html and css in editable format
This commit is contained in:
parent
7d67ede1ae
commit
66c91e45dc
0
antp/__init__.py
Normal file
0
antp/__init__.py
Normal 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)
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
14
antp/consts.py
Normal 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')
|
123
antp/exporter.py
123
antp/exporter.py
|
@ -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:
|
||||
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
|
||||
if model := select(invoke('modelNames')):
|
||||
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)
|
||||
|
|
|
@ -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)
|
||||
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.")
|
||||
|
||||
|
||||
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
4
tests/export.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
from antp.exporter import export_note_type
|
||||
|
||||
if __name__ == '__main__':
|
||||
export_note_type()
|
4
tests/import.py
Normal file
4
tests/import.py
Normal 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
12
tests/list_decks.py
Normal 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()
|
Loading…
Reference in a new issue