import dataclasses import re import frontmatter import marko import marko.md_renderer @dataclasses.dataclass(frozen=True) class CardContents: name: str | None description: str | None tags: list[str] assignments: list[str] class CardFileFormatter: """Component for formatting and parsing card files.""" def __init__(self, obsidian_mode=True): self.obsidian_mode = obsidian_mode self.markdown = marko.Markdown() self.renderer = marko.md_renderer.MarkdownRenderer() def format_card_contents(self, card: CardContents) -> str: ls = [] # Choose frontmatter data frontmatter_data = {} if card.name and self.obsidian_mode: frontmatter_data['aliases'] = [card.name] if card.tags: frontmatter_data['tags'] = card.tags if card.assignments: frontmatter_data['assignments'] = card.assignments if self.obsidian_mode: frontmatter_data['assignments'] = [ f'"[[{name}]]"' for name in frontmatter_data['assignments'] ] # Frontmatter if frontmatter_data: ls.append('---\n') for key, values in frontmatter_data.items(): ls.append(key) ls.append(':\n') for v in values: ls.append(' - ') ls.append(v) ls.append('\n') ls.append('---\n\n') # Card name if card.name: ls.append('# ') ls.append(card.name) ls.append('\n\n') # Card contents if card.description: ls.append(card.description) return ''.join(ls) def parse_card_contents(self, contents: str) -> CardContents: """ 1. Strips frontmatter 2. Parses header 3. Finds content. """ fm = frontmatter.loads(contents) del contents document = self.markdown.parse(fm.content.strip()) name = None for elem in document.children: if isinstance(elem, marko.block.Heading): name = self.renderer.render_children(elem) document.children.remove(elem) break assignments: list[str] = fm.metadata.get('assignments', []) for idx in range(0, len(assignments)): if m := re.match(r'^\[\[(.*)\]\]$', assignments[idx]): assignments[idx] = m.group(1) description = self.renderer.render_children(document).strip() return CardContents( name, description, tags=fm.metadata.get('tags', []), assignments=assignments, )