1
0
favro-sync/favro_sync/favro_markdown.py

171 lines
5.7 KiB
Python
Raw Normal View History

2024-09-27 14:13:03 +00:00
import dataclasses
2024-09-28 11:21:11 +00:00
import re
import datetime
2024-09-28 12:13:51 +00:00
import frontmatter
2024-09-27 14:13:03 +00:00
import marko
import marko.md_renderer
################################################################################
# FrontMatter keys
FM_KEY_TODO_LIST_COMPLETED = 'todo-list-completed'
FM_KEY_TAGS = 'tags'
FM_KEY_ASSIGNMENTS = 'assignments'
FM_KEY_DEPENDENCIES = 'dependencies'
FM_KEY_URL = 'url'
FM_KEY_ALIASES = 'aliases'
FM_KEY_ARCHIVED = 'archived'
FM_KEY_DUE_DATE = 'due'
FM_KEY_START_DATE = 'start-date'
################################################################################
2024-09-27 14:13:03 +00:00
2024-10-01 14:14:51 +00:00
2024-09-27 14:13:03 +00:00
@dataclasses.dataclass(frozen=True)
class CardContents:
2024-10-01 09:12:17 +00:00
identifier: str | None
name: str | None
description: str | None
tags: list[str]
assignments: list[str]
2024-09-28 12:10:13 +00:00
card_dependencies: list[str]
2024-09-28 12:27:59 +00:00
url: str
todo_list_completed: bool | None
archived: bool
start_date: datetime.date | None
due_date: datetime.date | None
2024-09-28 12:10:13 +00:00
def format_obsidian_link(text: str) -> str:
2024-09-28 12:27:59 +00:00
return f'[[{text}]]'
2024-09-28 12:10:13 +00:00
2024-09-28 12:13:51 +00:00
2024-09-28 12:10:13 +00:00
def parse_obsidian_link(text: str) -> str:
if m := re.match(r'^\[\[(.*)\]\]$', text):
return m.group(1)
return text
2024-09-27 14:13:03 +00:00
class CardFileFormatter:
"""Component for formatting and parsing card files."""
2024-09-27 14:13:03 +00:00
def __init__(self, obsidian_mode=True):
"""Initialize card formatter.
Arguments:
- `obsidian_mode`: Configure formatter to enable maximum compatibility
with [Obsidian](https://obsidian.md/). Enables internal links, and
exposes certain aliases.
"""
self.obsidian_mode = obsidian_mode
self.markdown = marko.Markdown()
self.renderer = marko.md_renderer.MarkdownRenderer()
def format_card_contents(self, card: CardContents) -> str:
"""Formats card contents. Mostly the inverse of [`parse_card_contents`]."""
# Choose frontmatter data
frontmatter_data = {}
2024-10-01 09:12:17 +00:00
if self.obsidian_mode:
aliases = []
if card.name:
aliases.append(card.name)
if card.identifier and card.name:
aliases.append(f'{card.identifier}: {card.name}')
if aliases:
frontmatter_data[FM_KEY_ALIASES] = aliases
2024-10-01 09:12:17 +00:00
del aliases
2024-09-30 11:57:34 +00:00
if card.tags:
frontmatter_data[FM_KEY_TAGS] = card.tags
2024-09-30 11:57:34 +00:00
if card.url and self.obsidian_mode:
frontmatter_data[FM_KEY_URL] = card.url
if card.assignments:
frontmatter_data[FM_KEY_ASSIGNMENTS] = card.assignments
if self.obsidian_mode:
frontmatter_data[FM_KEY_ASSIGNMENTS] = [
2024-09-28 12:13:51 +00:00
format_obsidian_link(name)
for name in frontmatter_data[FM_KEY_ASSIGNMENTS]
2024-09-28 12:10:13 +00:00
]
if card.card_dependencies:
frontmatter_data[FM_KEY_DEPENDENCIES] = card.card_dependencies
2024-09-28 12:10:13 +00:00
if self.obsidian_mode:
frontmatter_data[FM_KEY_DEPENDENCIES] = [
2024-09-28 12:13:51 +00:00
format_obsidian_link(name)
for name in frontmatter_data[FM_KEY_DEPENDENCIES]
]
if card.todo_list_completed:
frontmatter_data[FM_KEY_TODO_LIST_COMPLETED] = card.todo_list_completed
if card.archived:
frontmatter_data[FM_KEY_ARCHIVED] = card.archived
if card.due_date:
frontmatter_data[FM_KEY_DUE_DATE] = card.due_date
if card.start_date:
frontmatter_data[FM_KEY_DUE_DATE] = card.start_date
# Card name
2024-09-28 12:27:59 +00:00
ls = []
if card.name:
ls.append('# ')
ls.append(card.name)
ls.append('\n\n')
# Card contents
if description := card.description:
if self.obsidian_mode:
2024-10-01 14:14:51 +00:00
description = re.sub(
r'\-\s*\[\s*\]', '- [ ]', description, flags=re.MULTILINE
)
ls.append(description)
del description
2024-09-28 12:27:59 +00:00
fm = frontmatter.Post(''.join(ls), **frontmatter_data)
2024-09-28 12:27:59 +00:00
return frontmatter.dumps(fm)
2024-09-27 14:13:03 +00:00
def parse_card_contents(self, contents: str) -> CardContents:
"""Parses card contents. Mostly the inverse of [`parse_card_contents`].
2024-10-01 09:12:17 +00:00
1. Strips frontmatter and parses certain fields from the header.
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
2024-09-27 14:13:03 +00:00
tags: list[str] = fm.metadata.get(FM_KEY_TAGS, [])
2024-09-28 12:10:13 +00:00
assignments: list[str] = fm.metadata.get(FM_KEY_ASSIGNMENTS, [])
2024-09-28 12:10:13 +00:00
assignments = [parse_obsidian_link(text) for text in assignments]
card_dependencies: list[str] = fm.metadata.get(FM_KEY_DEPENDENCIES, [])
2024-09-28 12:13:51 +00:00
card_dependencies = [parse_obsidian_link(text) for text in card_dependencies]
2024-09-28 11:21:11 +00:00
url: list[str] = fm.metadata.get(FM_KEY_URL)
todo_list_completed: bool | None = fm.metadata.get(FM_KEY_TODO_LIST_COMPLETED)
archived: bool = fm.metadata.get(FM_KEY_ARCHIVED)
start_date: datetime.date = fm.metadata.get(FM_KEY_START_DATE)
due_date: datetime.date = fm.metadata.get(FM_KEY_DUE_DATE)
2024-09-30 11:57:34 +00:00
description = self.renderer.render_children(document).strip()
return CardContents(
2024-10-01 09:12:17 +00:00
None,
name,
description,
2024-09-28 12:10:13 +00:00
tags=tags,
2024-09-28 11:21:11 +00:00
assignments=assignments,
2024-09-28 12:10:13 +00:00
card_dependencies=card_dependencies,
2024-09-30 11:57:34 +00:00
url=url,
todo_list_completed=todo_list_completed,
archived=archived,
start_date=start_date,
due_date=due_date,
)