Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add AnkiConnect plugin support #29

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,18 +232,27 @@ The result:

Markdown2Anki uses [Frontmatter] metadata to add file specific metadata to your cards.
Frontmatter is essentially a YAML block that is enclosed by three dashes `---`.
If used, it must be placed at the beginning of your markdown file, before the first card.
It must be placed at the beginning of your markdown file, before the first card.
Frontmatter contains mandatory and optional metadata fields.

Here is a basic structure of a Frontmatter block:
Here is a basic structure of a Frontmatter block with mandatory fields:
```yaml
---
some_option: True
some_other_option: "Hello"
deck_name: "My deck"
note_type_basic: "Markdown2Anki - Basic"
note_type_cloze: "Markdown2Anki - Cloze"
---
```

Below is a list of the available options that can be set in the frontmatter block:

Mandatory fields:
- `deck_name` - The name of the deck where the cards will be imported.
- `note_type_basic` - The name of the note type for basic cards.
- `note_type_clode` - The name of the note type for cards with clozes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo


Optional fields:
- `tags` - A list of tags to be added to the cards.
- `no_tabs: True` - Disables tabs in generated cards. If set then:
- `L`, `R`, `-`, `+` tab flags are ignored, only `F` and `B` matter.
- Tab labels are ignored.
Expand Down
66 changes: 61 additions & 5 deletions src/markdown2anki/markdown_handler.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,72 @@
import frontmatter
from jsonschema import validate
from jsonschema.exceptions import ValidationError

from markdown2anki.utils import common_types as Types


# Note: Keep MarkdownMetadata and metadata_schema in sync when changing.
class MarkdownMetadata:
def __init__(
self,
deck_name: str,
note_type_basic: str,
note_type_cloze: str,
tags: list[str] | None = None,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful to add a md2anki tag to all cards made by markdown2anki by default, so that users can query them easily #21.

This should be easy to add as "default" and have the incoming list append to it

no_tabs: bool = False,
) -> None:
self.deck_name = deck_name
self.note_type_basic = note_type_basic
self.note_type_cloze = note_type_cloze
self.tags = tags
self.no_tabs = no_tabs


metadata_schema = {
"type": "object",
"properties": {
"deck_name": {"type": "string"},
"note_type_basic": {"type": "string"},
"note_type_cloze": {"type": "string"},
"tags": {"type": "array", "items": {"type": "string"}},
"no_tabs": {"type": "boolean"},
},
"required": [
"deck_name",
"note_type_basic",
"note_type_cloze",
],
"optional": ["tags", "no_tabs"],
"additionalProperties": False,
}


class MarkdownHandler:
def __init__(self, path: Types.PathString) -> None:
self.filepath = path
self._PostObject = frontmatter.load(path)
self.content = self._PostObject.content
self.metadata = self._PostObject.metadata
"""
From given path to markdown file create a MarkdownHandler object.

Both content and metadata are extracted from the file and stored in the object.
Metadata is validated against a schema.
"""
with open(path) as f:
self.metadata, self.content = frontmatter.parse(f.read())

if not self.metadata:
raise Exception("No metadata found in file.")

try:
validate(instance=self.metadata, schema=metadata_schema)
except ValidationError as e:
raise Exception(f"Invalid metadata schema: {e}")

# Unpack unstructred metadata and pass it through the MarkdownMetadata class to
# get the structured metadata and set defaults for optional fields, then convert
# it back to a dict.
self.metadata = MarkdownMetadata(**self.metadata).__dict__

def get_frontmatter_text(self):
def get_frontmatter_text(self) -> str:
"""Return frontmatter text."""
yaml_text = frontmatter.YAMLHandler().export(self.metadata)
frontmatter_text = "---\n" + yaml_text + "\n---\n\n"
return frontmatter_text
16 changes: 16 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest as pyt
from typing import Callable


@pyt.fixture(scope="function") # This can be function, session, module, class, package
Expand All @@ -17,3 +18,18 @@ def template_dir(tmp_path_factory):
@pyt.fixture
def tmp_configs(template_dir):
return template_dir / ""


@pyt.fixture(scope="function")
def temp_md_file(tmp_path) -> Callable:
"""
Return a function that can create a temporary markdown file with the provided
content.
"""

def _create_md_file(content: str) -> str:
md_file = tmp_path / "temp_file.md"
md_file.write_text(content)
return str(md_file)

return _create_md_file
79 changes: 79 additions & 0 deletions tests/unit_tests/test_markdown_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import pytest
from markdown2anki.markdown_handler import MarkdownHandler

# NOTE: In all below strings it is important that the string starts in the same line as
# the triple quotes. Otherwise, the frontmatter will not be detected due to the extra
# newline.


content_0 = """Some dummy content, no frontmatter."""


def test_given_file_with_no_frontmatter_raises_exception(temp_md_file):
with pytest.raises(Exception):
_ = MarkdownHandler(temp_md_file(content_0))


content_1 = """---
invalid_frontmatter
---

Some dummy content
"""


def test_given_file_with_invalid_yaml_raises_exception(temp_md_file):
with pytest.raises(Exception):
_ = MarkdownHandler(temp_md_file(content_1))


content_2 = """---
deck_name: deck
note_type_basic: basic
note_type_cloze: cloze
---

Some dummy content
"""


def test_given_file_with_valid_yaml_succeeds(temp_md_file):
handle = MarkdownHandler(temp_md_file(content_2))

assert handle.metadata["deck_name"] == "deck"
assert handle.metadata["note_type_basic"] == "basic"
assert handle.metadata["note_type_cloze"] == "cloze"


content_3 = """---
some_wrong_key: deck
note_type_basic: basic
note_type_cloze: cloze
---

Some dummy content
"""


def test_given_file_with_invalid_yaml_schema_raises_exception(temp_md_file):
with pytest.raises(Exception):
_ = MarkdownHandler(temp_md_file(content_3))


content_4 = """---
deck_name: deck
note_type_basic: basic
note_type_cloze: cloze
tags:
- tag1
- tag2
---

Some dummy content
"""


def test_given_file_with_optional_tags_succeeds(temp_md_file):
handle = MarkdownHandler(temp_md_file(content_4))

assert handle.metadata["tags"] == ["tag1", "tag2"]
Loading