178 lines
6.0 KiB
Python
178 lines
6.0 KiB
Python
import dataclasses
|
|
import datetime
|
|
import re
|
|
|
|
import frontmatter
|
|
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'
|
|
|
|
################################################################################
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class CardContents:
|
|
identifier: str | None
|
|
name: str | None
|
|
description: str | None
|
|
tags: list[str]
|
|
assignments: list[str]
|
|
card_dependencies: list[str]
|
|
url: str
|
|
todo_list_completed: bool | None
|
|
is_archived: bool
|
|
start_date: datetime.date | None
|
|
due_date: datetime.date | None
|
|
custom_fields: dict[str,str]
|
|
|
|
|
|
def format_obsidian_link(text: str) -> str:
|
|
return f'[[{text}]]'
|
|
|
|
|
|
def parse_obsidian_link(text: str) -> str:
|
|
if m := re.match(r'^\[\[(.*)\]\]$', text):
|
|
return m.group(1)
|
|
return text
|
|
|
|
|
|
class CardFileFormatter:
|
|
"""Component for formatting and parsing card files."""
|
|
|
|
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 = {}
|
|
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
|
|
del aliases
|
|
if len(card.tags) > 0:
|
|
frontmatter_data[FM_KEY_TAGS] = card.tags
|
|
if card.url is not None and self.obsidian_mode:
|
|
frontmatter_data[FM_KEY_URL] = card.url
|
|
if len(card.assignments) > 0:
|
|
frontmatter_data[FM_KEY_ASSIGNMENTS] = card.assignments
|
|
if self.obsidian_mode:
|
|
frontmatter_data[FM_KEY_ASSIGNMENTS] = [
|
|
format_obsidian_link(name)
|
|
for name in frontmatter_data[FM_KEY_ASSIGNMENTS]
|
|
]
|
|
if len(card.card_dependencies) > 0:
|
|
frontmatter_data[FM_KEY_DEPENDENCIES] = card.card_dependencies
|
|
if self.obsidian_mode:
|
|
frontmatter_data[FM_KEY_DEPENDENCIES] = [
|
|
format_obsidian_link(name)
|
|
for name in frontmatter_data[FM_KEY_DEPENDENCIES]
|
|
]
|
|
if card.todo_list_completed is not None:
|
|
frontmatter_data[FM_KEY_TODO_LIST_COMPLETED] = card.todo_list_completed
|
|
if card.is_archived is not None:
|
|
frontmatter_data[FM_KEY_ARCHIVED] = card.is_archived
|
|
if card.due_date is not None:
|
|
frontmatter_data[FM_KEY_DUE_DATE] = card.due_date
|
|
if card.start_date is not None:
|
|
frontmatter_data[FM_KEY_DUE_DATE] = card.start_date
|
|
if len(card.custom_fields) > 0:
|
|
frontmatter_data |= card.custom_fields
|
|
|
|
# Card name
|
|
ls = []
|
|
if card.name:
|
|
ls.append('# ')
|
|
ls.append(card.name)
|
|
ls.append('\n\n')
|
|
|
|
# Card contents
|
|
if description := card.description:
|
|
if self.obsidian_mode:
|
|
description = re.sub(
|
|
r'\-\s*\[\s*\]',
|
|
'- [ ]',
|
|
description,
|
|
flags=re.MULTILINE,
|
|
)
|
|
ls.append(description)
|
|
del description
|
|
fm = frontmatter.Post(''.join(ls), **frontmatter_data)
|
|
|
|
return frontmatter.dumps(fm)
|
|
|
|
def parse_card_contents(self, contents: str) -> CardContents:
|
|
"""Parses card contents. Mostly the inverse of [`parse_card_contents`].
|
|
|
|
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
|
|
|
|
tags: list[str] = fm.metadata.get(FM_KEY_TAGS, [])
|
|
|
|
assignments: list[str] = fm.metadata.get(FM_KEY_ASSIGNMENTS, [])
|
|
assignments = [parse_obsidian_link(text) for text in assignments]
|
|
|
|
card_dependencies: list[str] = fm.metadata.get(FM_KEY_DEPENDENCIES, [])
|
|
card_dependencies = [parse_obsidian_link(text) for text in card_dependencies]
|
|
|
|
url: list[str] = fm.metadata.get(FM_KEY_URL)
|
|
todo_list_completed: bool | None = fm.metadata.get(FM_KEY_TODO_LIST_COMPLETED)
|
|
is_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)
|
|
|
|
description = self.renderer.render_children(document).strip()
|
|
return CardContents(
|
|
None,
|
|
name,
|
|
description,
|
|
tags=tags,
|
|
assignments=assignments,
|
|
card_dependencies=card_dependencies,
|
|
url=url,
|
|
todo_list_completed=todo_list_completed,
|
|
is_archived=is_archived,
|
|
start_date=start_date,
|
|
due_date=due_date,
|
|
custom_fields={}, # TODO
|
|
)
|