feat(markdown): added presentation metadata

This commit is contained in:
Jordan ⌨️ 2024-02-18 01:01:52 +01:00
parent bcb3263149
commit 87c8e9e8cf
3 changed files with 289 additions and 88 deletions

View file

@ -15,5 +15,9 @@ def main(argv: list[str]) -> NoReturn:
print(f"{file} does not exist", file=sys.stderr) print(f"{file} does not exist", file=sys.stderr)
exit(1) exit(1)
MarkdownParser(file.read_text()) slides = MarkdownParser(file.read_text()).parse()
from pprint import pprint
pprint(slides)
exit(0) exit(0)

View file

@ -11,19 +11,23 @@ Char = TypeVar("Char", str, bytes)
class MarkdownElement: ... class MarkdownElement: ...
@dataclass @dataclass
class MarkdownText(MarkdownElement): class MarkdownText(MarkdownElement):
content: str content: str
@dataclass @dataclass
class MarkdownComment(MarkdownElement): class MarkdownComment(MarkdownElement):
content: str content: str
@dataclass @dataclass
class MarkdownHeader(MarkdownElement): class MarkdownHeader(MarkdownElement):
level: Literal[1, 2, 3, 4, 5] level: Literal[1, 2, 3, 4, 5]
content: str content: str
@dataclass @dataclass
class MarkdownListElement(MarkdownElement): class MarkdownListElement(MarkdownElement):
class ListType(StrEnum): class ListType(StrEnum):
@ -34,23 +38,35 @@ class MarkdownListElement(MarkdownElement):
content: str content: str
index: Optional[int] index: Optional[int]
@dataclass @dataclass
class MarkdownCodeBlock(MarkdownElement): class MarkdownCodeBlock(MarkdownElement):
lang: str lang: str
content: str content: str
@dataclass @dataclass
class MarkdownPreprocessor(MarkdownElement): class MarkdownPreprocessor(MarkdownElement):
program: str program: str
content: str content: str
@dataclass @dataclass
class MarkdownSlide(MarkdownElement): class MarkdownSlide(MarkdownElement):
elements: list[MarkdownElement] elements: list[MarkdownElement]
@dataclass @dataclass
class MarkdownNewLine(MarkdownElement): class MarkdownNewLine(MarkdownElement): ...
...
# --- Presentation class ----------------------------------------------------
@dataclass
class Presentation:
slides: list[MarkdownSlide]
metadata: dict[str, str]
# --- Markdown parser ---------------------------------------------------------- # --- Markdown parser ----------------------------------------------------------
@ -89,7 +105,7 @@ class MarkdownParser:
if char == c or char is None: if char == c or char is None:
break break
content += char content += char
return content return content
def eat_line(self) -> str: def eat_line(self) -> str:
@ -98,13 +114,13 @@ class MarkdownParser:
def eat_until_pattern(self, pattern: str) -> str: def eat_until_pattern(self, pattern: str) -> str:
content = "" content = ""
while char := self.next(): while char := self.next():
if char is None: if char is None:
break break
if self.text[self.offset:].startswith(pattern): if self.text[self.offset :].startswith(pattern):
self.offset += len(pattern) self.offset += len(pattern)
break break
content += char content += char
return content return content
def eat_number(self) -> int: def eat_number(self) -> int:
@ -118,8 +134,9 @@ class MarkdownParser:
return int(nbr) return int(nbr)
def parse(self) -> list[MarkdownSlide]: def parse(self) -> Presentation:
slides: list[MarkdownSlide] = [] slides: list[MarkdownSlide] = []
metadata: dict[str, str] = {}
current_slide: list[MarkdownElement] = [] current_slide: list[MarkdownElement] = []
while c := self.peek(): while c := self.peek():
@ -133,16 +150,32 @@ class MarkdownParser:
case "-": case "-":
count = self.eat_count("-") count = self.eat_count("-")
if count == 1: if count == 1:
self.eat(' ') self.eat(" ")
content = self.eat_line() content = self.eat_line()
current_slide.append(MarkdownListElement(MarkdownListElement.ListType.UNORDERED, content, None)) current_slide.append(
MarkdownListElement(
MarkdownListElement.ListType.UNORDERED, content, None
)
)
elif count == 3: elif count == 3:
self.eat("\n") self.eat("\n")
slides.append(MarkdownSlide(current_slide.copy())) if len(slides) == 0 and len(current_slide) == 0:
current_slide.clear() raw_metadata = self.eat_until_pattern("---").split("\n")
continue metadata = {
line.split(":")[0].strip(): line.split(":")[1].strip()
for line in raw_metadata
if line
}
self.eat("\n")
continue
else:
slides.append(MarkdownSlide(current_slide.copy()))
current_slide.clear()
continue
else: else:
current_slide.append(MarkdownText("-" * count + self.eat_line())) current_slide.append(
MarkdownText("-" * count + self.eat_line())
)
case "`": case "`":
count = self.eat_count("`") count = self.eat_count("`")
@ -151,17 +184,25 @@ class MarkdownParser:
content = self.eat_until_pattern("```") content = self.eat_until_pattern("```")
current_slide.append(MarkdownCodeBlock(lang, content.strip())) current_slide.append(MarkdownCodeBlock(lang, content.strip()))
else: else:
current_slide.append(MarkdownText("`" * count + self.eat_line())) current_slide.append(
MarkdownText("`" * count + self.eat_line())
)
case "*": case "*":
count = self.eat_count("*") count = self.eat_count("*")
if count == 1 and self.peek() == " ": if count == 1 and self.peek() == " ":
self.eat(' ') self.eat(" ")
content = self.eat_line() content = self.eat_line()
current_slide.append(MarkdownListElement(MarkdownListElement.ListType.UNORDERED, content, None)) current_slide.append(
MarkdownListElement(
MarkdownListElement.ListType.UNORDERED, content, None
)
)
else: else:
current_slide.append(MarkdownText("*" * count + self.eat_line())) current_slide.append(
MarkdownText("*" * count + self.eat_line())
)
case "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9": case "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9":
index = self.eat_number() index = self.eat_number()
@ -169,12 +210,16 @@ class MarkdownParser:
self.eat(".") self.eat(".")
self.eat(" ") self.eat(" ")
content = self.eat_line() content = self.eat_line()
current_slide.append(MarkdownListElement(MarkdownListElement.ListType.ORDERED, content, index)) current_slide.append(
MarkdownListElement(
MarkdownListElement.ListType.ORDERED, content, index
)
)
else: else:
current_slide.append(MarkdownText(str(count) + self.eat_line())) current_slide.append(MarkdownText(str(count) + self.eat_line()))
case '<': case "<":
if self.text[self.offset:].startswith("<!--"): if self.text[self.offset :].startswith("<!--"):
self.offset += 4 self.offset += 4
self.eat(" ") self.eat(" ")
content = self.eat_until_pattern("-->") content = self.eat_until_pattern("-->")
@ -182,21 +227,23 @@ class MarkdownParser:
else: else:
current_slide.append(MarkdownText(self.eat_line())) current_slide.append(MarkdownText(self.eat_line()))
case '~': case "~":
count = self.eat_count("~") count = self.eat_count("~")
if count == 3: if count == 3:
program = self.eat_line() program = self.eat_line()
content = self.eat_until_pattern("~~~") content = self.eat_until_pattern("~~~")
current_slide.append(MarkdownPreprocessor(program, content)) current_slide.append(MarkdownPreprocessor(program, content))
else: else:
current_slide.append(MarkdownText("~" * count + self.eat_line())) current_slide.append(
MarkdownText("~" * count + self.eat_line())
)
case "\n": case "\n":
self.eat("\n") self.eat("\n")
current_slide.append(MarkdownNewLine()) current_slide.append(MarkdownNewLine())
case _: case _:
current_slide.append(MarkdownText(self.eat_line())) current_slide.append(MarkdownText(self.eat_line()))
slides.append(MarkdownSlide(current_slide)) slides.append(MarkdownSlide(current_slide))
return slides return Presentation(slides, metadata)

View file

@ -2,131 +2,281 @@ import unittest
from cuteslides import markdown from cuteslides import markdown
from inspect import cleandoc from inspect import cleandoc
class TestMarkdown(unittest.TestCase): class TestMarkdown(unittest.TestCase):
maxDiff = None maxDiff = None
def test_paragraphs(self): def test_paragraphs(self):
self.assertEqual( self.assertEqual(
markdown.MarkdownParser("Hello, World!").parse(), markdown.MarkdownParser("Hello, World!").parse().slides,
[markdown.MarkdownSlide([markdown.MarkdownText("Hello, World!")])], [markdown.MarkdownSlide([markdown.MarkdownText("Hello, World!")])],
) )
self.assertEqual( self.assertEqual(
markdown.MarkdownParser("Hello,\nWorld!").parse(), markdown.MarkdownParser("Hello,\nWorld!").parse().slides,
[markdown.MarkdownSlide([markdown.MarkdownText("Hello,"), markdown.MarkdownText("World!")])], [
markdown.MarkdownSlide(
[markdown.MarkdownText("Hello,"), markdown.MarkdownText("World!")]
)
],
) )
self.assertEqual( self.assertEqual(
markdown.MarkdownParser("Hello,\n\nWorld!").parse(), markdown.MarkdownParser("Hello,\n\nWorld!").parse().slides,
[markdown.MarkdownSlide([markdown.MarkdownText("Hello,"), markdown.MarkdownNewLine(), markdown.MarkdownText("World!")])], [
markdown.MarkdownSlide(
[
markdown.MarkdownText("Hello,"),
markdown.MarkdownNewLine(),
markdown.MarkdownText("World!"),
]
)
],
) )
def test_headers(self): def test_headers(self):
self.assertEqual( self.assertEqual(
markdown.MarkdownParser("# Hello, World!").parse(), markdown.MarkdownParser("# Hello, World!").parse().slides,
[markdown.MarkdownSlide([markdown.MarkdownHeader(1, "Hello, World!")])], [markdown.MarkdownSlide([markdown.MarkdownHeader(1, "Hello, World!")])],
) )
self.assertEqual( self.assertEqual(
markdown.MarkdownParser("## Hello, World!").parse(), markdown.MarkdownParser("## Hello, World!").parse().slides,
[markdown.MarkdownSlide([markdown.MarkdownHeader(2, "Hello, World!")])], [markdown.MarkdownSlide([markdown.MarkdownHeader(2, "Hello, World!")])],
) )
self.assertEqual( self.assertEqual(
markdown.MarkdownParser("### Hello, World!").parse(), markdown.MarkdownParser("### Hello, World!").parse().slides,
[markdown.MarkdownSlide([markdown.MarkdownHeader(3, "Hello, World!")])], [markdown.MarkdownSlide([markdown.MarkdownHeader(3, "Hello, World!")])],
) )
def test_lists(self): def test_lists(self):
self.assertEqual( self.assertEqual(
markdown.MarkdownParser(cleandoc("""- Milk markdown.MarkdownParser(
cleandoc(
"""- Milk
- Bread - Bread
- Cheese""")).parse(), - Cheese"""
[markdown.MarkdownSlide([ )
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Milk", None), )
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Bread", None), .parse()
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Cheese", None), .slides,
])], [
markdown.MarkdownSlide(
[
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.UNORDERED,
"Milk",
None,
),
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.UNORDERED,
"Bread",
None,
),
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.UNORDERED,
"Cheese",
None,
),
]
)
],
) )
self.assertEqual( self.assertEqual(
markdown.MarkdownParser(cleandoc("""* Milk markdown.MarkdownParser(
cleandoc(
"""* Milk
* Bread * Bread
* Cheese""")).parse(), * Cheese"""
[markdown.MarkdownSlide([ )
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Milk", None), )
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Bread", None), .parse()
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Cheese", None), .slides,
])], [
markdown.MarkdownSlide(
[
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.UNORDERED,
"Milk",
None,
),
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.UNORDERED,
"Bread",
None,
),
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.UNORDERED,
"Cheese",
None,
),
]
)
],
) )
self.assertEqual( self.assertEqual(
markdown.MarkdownParser(cleandoc("""1. Milk markdown.MarkdownParser(
cleandoc(
"""1. Milk
2. Bread 2. Bread
3. Cheese""")).parse(), 3. Cheese"""
[markdown.MarkdownSlide([ )
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.ORDERED, "Milk", 1), )
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.ORDERED, "Bread", 2), .parse()
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.ORDERED, "Cheese", 3), .slides,
])], [
markdown.MarkdownSlide(
[
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.ORDERED, "Milk", 1
),
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.ORDERED, "Bread", 2
),
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.ORDERED, "Cheese", 3
),
]
)
],
) )
def test_comments(self): def test_comments(self):
self.assertEqual( self.assertEqual(
markdown.MarkdownParser(cleandoc("""<!-- This is a comment --> markdown.MarkdownParser(
Hello, World!""")).parse(), cleandoc(
[markdown.MarkdownSlide([ """<!-- This is a comment -->
markdown.MarkdownComment("This is a comment"), Hello, World!"""
markdown.MarkdownNewLine(), )
markdown.MarkdownText("Hello, World!"), )
])], .parse()
.slides,
[
markdown.MarkdownSlide(
[
markdown.MarkdownComment("This is a comment"),
markdown.MarkdownNewLine(),
markdown.MarkdownText("Hello, World!"),
]
)
],
) )
self.assertEqual( self.assertEqual(
markdown.MarkdownParser(cleandoc("""Hello, World! markdown.MarkdownParser(
<!-- This is a comment -->""")).parse(), cleandoc(
[markdown.MarkdownSlide([ """Hello, World!
markdown.MarkdownText("Hello, World!"), <!-- This is a comment -->"""
markdown.MarkdownComment("This is a comment"), )
])], )
.parse()
.slides,
[
markdown.MarkdownSlide(
[
markdown.MarkdownText("Hello, World!"),
markdown.MarkdownComment("This is a comment"),
]
)
],
) )
def test_code_blocks(self): def test_code_blocks(self):
self.assertEqual( self.assertEqual(
markdown.MarkdownParser(cleandoc("""```python markdown.MarkdownParser(
cleandoc(
"""```python
print("Hello, World!") print("Hello, World!")
```""")).parse(), ```"""
[markdown.MarkdownSlide([ )
markdown.MarkdownCodeBlock("python", 'print("Hello, World!")'), )
])], .parse()
.slides,
[
markdown.MarkdownSlide(
[
markdown.MarkdownCodeBlock("python", 'print("Hello, World!")'),
]
)
],
) )
def test_preprocessors(self): def test_preprocessors(self):
self.assertEqual( self.assertEqual(
markdown.MarkdownParser(cleandoc("""~~~bash markdown.MarkdownParser(
cleandoc(
"""~~~bash
echo 'Hello, World!' echo 'Hello, World!'
~~~ ~~~
Hi""")).parse(), Hi"""
[markdown.MarkdownSlide([ )
markdown.MarkdownPreprocessor("bash", "echo 'Hello, World!'"), )
markdown.MarkdownNewLine(), .parse()
markdown.MarkdownText("Hi"), .slides,
])], [
markdown.MarkdownSlide(
[
markdown.MarkdownPreprocessor("bash", "echo 'Hello, World!'"),
markdown.MarkdownNewLine(),
markdown.MarkdownText("Hi"),
]
)
],
) )
def test_slide(self): def test_slide(self):
self.assertEqual( self.assertEqual(
markdown.MarkdownParser(cleandoc("""# Hello, World! markdown.MarkdownParser(
cleandoc(
"""# Hello, World!
--- ---
- Milk - Milk
- Bread - Bread
- Cheese""")).parse(), - Cheese"""
)
)
.parse()
.slides,
[ [
markdown.MarkdownSlide([markdown.MarkdownHeader(1, "Hello, World!")]), markdown.MarkdownSlide([markdown.MarkdownHeader(1, "Hello, World!")]),
markdown.MarkdownSlide([ markdown.MarkdownSlide(
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Milk", None), [
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Bread", None), markdown.MarkdownListElement(
markdown.MarkdownListElement(markdown.MarkdownListElement.ListType.UNORDERED, "Cheese", None), markdown.MarkdownListElement.ListType.UNORDERED,
]), "Milk",
None,
),
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.UNORDERED,
"Bread",
None,
),
markdown.MarkdownListElement(
markdown.MarkdownListElement.ListType.UNORDERED,
"Cheese",
None,
),
]
),
], ],
) )
def test_metadata(self):
self.assertEqual(
markdown.MarkdownParser(
cleandoc(
"""---
title: Hello, World!
author: Keyb
date: 01 Jan 1970
---
"""
)
)
.parse()
.metadata,
{"title": "Hello, World!", "author": "Keyb", "date": "01 Jan 1970"}
)