Pull request #41: Feature/slides

Merge in FRAG/50ohm.de from feature/slides to main

* commit 'e5e96f5689d0a1323b5d3b03884f24eae7781d38': (26 commits)
  feat: create build directory if not existing
  refactor: relative import
  feat: update tests
  feat: use standard render method for rendering slides
  refactor: clean up code
  refactor: format file with ruff
  feat: remove background color attributes
  refactor: use dictionary unpacking
  refactor: use splat operator for class args
  feat: add configurable paths with pathlib
  feat: freeze new requirements
  refactor: removed uncessessary classes from slide renderer
  refactor: fixed bugs due to merge
  fix: fixed a wrong merge
  feature: build slide index
  feature: last slide with information
  feature: added tests for slides
  feature: finished slide building process
  fix: removed acceptance test result
  refactor: gitignore
  ...
This commit is contained in:
Prof.Dr. Matthias Jung DL9MJ
2025-06-24 22:09:49 +02:00
committed by Tim Grelka
22 changed files with 1020 additions and 123 deletions
+1
View File
@@ -162,6 +162,7 @@ cython_debug/
#.idea/
test/acceptanceTest.html
test/acceptanceTestSlides.html
.DS_Store
.envrc
config/config.json
+1 -1
View File
@@ -7,7 +7,7 @@ conf = config.Config()
api = directus.DirectusAPI(conf.question_base_url, conf.question_access_token)
content_api = directus.DirectusAPI(conf.content_base_url, conf.content_access_token)
dl = download.Download(api, content_api, conf)
bd = build.Build()
bd = build.Build(conf)
# Download the text files:
dl.download_edition("N")
+234
View File
@@ -0,0 +1,234 @@
# Erstellen von Folien
Das Erstellen von Folien ist direkt im Directus möglich. Wir verwenden reveal.js als Framework, um die Inhalte darzustellen.
## Directus
Im Directus gibt es zu jeder Sektion ein Feld _Slide_ in dem vereinfacht die Inhalte von Folien eingegeben werden können.
## Unterstützte Formate
### Folientrennung
Jede Folie (außer die erste) beginnt mit `---`. Dadurch wird eine neue Folie erzeugt.
#### Spezielle Folien
Reveal.js erlaubt es, Folien bestimmte Attribute, wie beispielsweise die Hintergrundfarbe zu übermitteln. Das kann direkt beim Erstellen einer Folie angegeben werden.
``` MarkDown
--- data-background="#FF0000"
```
Dadurch wird die Folie in knallrot gefärbt. Das sollte nur in Ausnahmen verwendet werden.
Alternativ werden drei Begriffe ausgewertet, die bereits auf der Webseitenansicht zum Einsatz kommen.
``` MarkDown
--- unit
```
Wird verwendet, um eine neue Einheit einzuführen.
Färbt die Folie hellgrün ein.
``` MarkDown
--- attention
```
Wird verwendet, um die Aufmerksamkeit zu erregen. Beispielsweise bei Hinweisen auf die Gültigkeit bei Inkrafttreten der AfuV.
Färbt die Folie hellblau ein.
``` attention
--- danger
```
Wird verwendet, um auf Gefahren hinzuweisen. Beispielsweise beim Umgang mit elektrischem Strom.
Färbt die Folie hellrot ein.
### Überschriften
Überschriften werden mit dem `#`-Zeichen eingeleitet. Dabei gibt die Häufigkeit des Zeichens die Ebene der Überschrift an.
``` MarkDown
# Überschrift 1
## Überschrift 2
### Überschrift 3
```
Die maximale Ebene ist 6.
### Listen
Häufig sind auf Folien Listen zu sehen. Diese lassen sich mit `*` einleiten.
``` MarkDown
* Listenpunkt
* Noch ein Listenpunkt
```
TODO: Verschachtelte Listen sind noch nicht möglich
### Tabellen
Tabellen werden mit Zeichen `|` zur Unterteilung der Spalten gesetzt. Die erste Spalte ist eine Kopfspalte. In ihr kann angegeben werden, ob der Inhalt der Spalte linksbündig (`l`), rechtsbündig (`r`) oder mittig (`c`) ausgerichtet sein soll.
``` Markdown
| l: Wert | l: Bereich | l: Bedeutung | l: Englisch |
| R | 1 - 5 | Lesbarkeit | Readability |
| S | 1 - 9 | Signalstärke | Signal Strength |
| T | 1 - 9 | Tonqualität | Tone |
[table:n_rst:Die Bestandteile des RST-Rapports]
```
Die Tabelle muss mit `[table:referenz:Beschreibung]` abgeschlossen werden. Dadurch wird intern eine Referenz gesetzt, auf die verwiesen werden kann.
### Textauszeichnung
Es kann einfache Textauszeichnung, wie _hervorgehoben_ oder <u>unterstrichen</u> verwendet werden.
``` MarkDown
Es kann einfache Textauszeichnung, wie *hervorgehoben* oder <u>unterstrichen</u> verwendet werden.
```
### Bilder
Bilder werden mit einer Referenz auf das Bild in der Datenbank eingefügt.
``` MarkDown
[photo:123:n_rst_s-meter:Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt]
```
Es kann `photo` oder `picture` verwendet werden, je nach Datenbanktabelle in der das Bild abgelegt ist. Als nächstes wird die Nummer des Bildes angegeben. Darauf folgt eine Referenz für Verweise. Und abschließend die Beschreibung, die unter dem Bild angezeigt wird. Die Bilder werden pro Dokument durchnummeriert.
### Fragen aus dem Prüfungskatalog
Fragen aus dem Fragenkatalog können durch eine Referenz auf die Fragennummer eingefügt werden.
``` Markdown
[question:2482]
```
Fragen sollten auf einer eigenen Folie angezeigt werden, da sie in der Regel sehr groß sind.
``` Markdown
---
[question:2482]
---
```
Die Antworten werden beim Erstellen der Folien in eine zufällige Reihenfolge gebracht, sodass nicht wie im Prüfungskatalog die erste Antwort die richtige ist.
### QSO
Ein beispielhaftes QSO kann folgendermaßen hinzugefügt werden:
``` Markdown
<qso>
Ist diese Frequenz frei? DL1PZ
> *(keine Antwort)*
Ist diese Frequenz frei? DL1PZ
> *(keine Antwort)*
CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
> DL1PZ hier ist DL9MJ bitte kommen
</qso>
```
Jede Zeile ist eine Sprechblase. Die einzelnen Sprechblasen werden nacheinander aufgedeckt. Zeilen, die mit `>` beginnen, werden als Teil der Gegenstelle angezeigt.
### Einblendeeffekt
Soll ein Teil der Folie erst später aufgedeckt werden, kann dieser in `<fragment>` eingebettet werden.
``` MarkDown
<fragment>
Dieser Text erscheint erst später.
</fragment>
```
Mehrere `<fragment>`-Abschnitte nacheinander sorgen dafür, dass die Inhalte Schritt für Schritt aufgedeckt werden.
### Notizen
Es ist möglich, Notizen für den oder die Vortragende hinzuzufügen. Die sind nur in der "Speaker-Ansicht" (Taste 's' drücken) sichtbar.
Sinnvoll sind kurze Hinweise auf die Erklärungen, die in der Webseitenversion gegeben werden oder Hintergrundinformationen. Die brauchen nicht auf die Folien. Und kurze Hinweise sollten für Funkamateure ausreichend sein, um die Notiz zu verstehen und den Inhalt zu vermitteln.
``` MarkDown
<note>
Die Notiz wird nur in der Speaker-Ansicht angezeigt
* Listen sind auch möglich
* Am besten kurz halten
* Der Platz ist begrenzt
</note>
```
### Formeln
Es können Formeln im LaTeX-Stil angegeben werden. Diese werden in `$` gesetzt.
``` MarkDown
$\lambda[[\text{m}]] = \dfrac{300}{f[[\text{MHz}]]} = \dfrac{300}{145,3 \ \text{MHz}} \approx 2,06 \ \text{m}$
```
Abweichend von LaTeX müssen die rechteckigen Klammern gedoppelt werden.
### Links
Es können Links eingefügt werden.
``` MarkDown
[Link zum DARC](https://darc.de/)
```
### Zwei Spalten
Manchmal ist es sinnvoll, zwei Spalten zu verwenden. Beispielsweise bei einem Bild mit Beschreibung. Dazu gibt es die beiden Tags `<left>` und `<right>`.
``` MarkDown
<left>
[photo:167:rufzeichen_flugzeug:Flugzeug mit dem Rufzeichen DEBPF]
</left>
<right>
* Funkstationen verwenden Rufzeichen, um sich zu identifizieren
* Folge von Buchstaben und Ziffern
* Jedes mit Funk ausgerüstete Flugzeug und Schiff hat ein Rufzeichen
</right>
```
### Webseite interaktiv einbetten
Es kann eine ganze Webseite interaktiv eingebettet werden, sodass darin agiert werden kann. Zusätzlich sind die Navigationspfeile und der Speaker-View erhalten.
``` MarkDown
--- data-background-iframe="https://f5len.org/tools/locator/" data-background-interactive
<note>
* Es wird eine interaktive Karte mit Maidenhead-Locator gezeigt.
</note>
```
Zusätzlich eine Speaker-Note einzufügen ist sinnvoll, um keine leere Slide zu erzeugen und den Speaker darauf hinzuweisen, dass die Webseite nur vom Hauptbildschirm bedient werden kann.
### Kleinere Schrift
In ganz seltenen Ausnahmen kann eine kleinere Schrift verwendet werden. Das kann dann angewandt werden, wenn ein oder zwei Zeilen einer Tabelle nicht mehr auf die Folie passen.
``` MarkDown
--- style="font-size: smaller;"
* Längere
* Liste
```
### Direkter Übergang
Ohne verschieben der Folie zur nächsten Schalten. Das kann als Effekt genutzt werden, damit auf der Folie ein Austauscheffekt stattfindet.
```MarkDown
---
## Folie
Mit Text
--- data-transition="none"
## Folie
Mit anderem Text
```
+1 -1
View File
@@ -42,4 +42,4 @@ select = [
[tool.pytest.ini_options]
addopts = ["--color=yes"]
markers = ["html", "latex"]
markers = ["html", "latex", "slide"]
+6 -9
View File
@@ -1,5 +1,3 @@
from jinja2 import Environment, FileSystemLoader
from mistletoe import Document, HtmlRenderer
@@ -30,12 +28,8 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
ref_id = 0
def __init__(
self,
question_renderer=None,
picture_handler=None,
photo_handler=None,
include_handler=None
) :
self, *extras, question_renderer=None, picture_handler=None, photo_handler=None, include_handler=None, **kwargs
):
super().__init__(
Dash,
BlockComment,
@@ -53,8 +47,11 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
Photo,
Table,
Qso,
Include
Include,
*extras,
**kwargs,
)
self.question_renderer = question_renderer
self.picture_handler = picture_handler
self.photo_handler = photo_handler
+40
View File
@@ -0,0 +1,40 @@
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
from .slide_break import SlideBreak
class FiftyOhmHtmlSlideRenderer(FiftyOhmHtmlRenderer):
def __init__(self, **kwargs):
super().__init__(
SlideBreak,
**kwargs,
)
def render_slide_break(self, token):
inner = self.render_inner(token)
if token.attribute is None:
return f"<section>\n{inner}\n</section>\n"
else:
return f"<section {token.attribute}>\n{inner}\n</section>\n"
def render_qso(self, token):
qso = '<div class="qso r-fit-text">\n'
for child in token.children:
direction = "other" if child.received else "own"
fade = "fragment fade-left" if child.received else "fragment fade-right"
qso += f'<div class="qso_{direction} {fade}">{self.render_inner(child)}</div>\n'
qso += "</div>\n"
return qso
def render_tag(self, token):
if token.tagtype == "fragment":
return f'<div class="fragment">\n{self.render_inner(token)}\n</div>\n'
if token.tagtype == "left":
return f'<div id="left">\n{self.render_inner(token)}\n</div>\n'
if token.tagtype == "right":
return f'<div id="right">\n{self.render_inner(token)}\n</div>\n'
elif token.tagtype == "note":
return f'<aside class="notes">\n{self.render_inner(token)}\n</aside>\n'
return "" # Ignore other tags in slide context anyway in tokenizer
+33
View File
@@ -0,0 +1,33 @@
import re
from mistletoe.block_token import BlockToken, tokenize
class SlideBreak(BlockToken):
@staticmethod
def start(line):
return re.match(r"^---.*$", line)
@classmethod
def read(cls, lines):
child_lines = []
first_line = next(lines) # Consume the line that started the slide break
match = re.match(r"^---(.*)$", first_line)
attribute = match.group(1).strip() if match.group(1) else None
# We have to work with peak here, since a normal for loop would already
# consume the next line, which might be a `---` line that marks the
# beginning of a new slide as well
next_line = lines.peek()
while next_line is not None:
if cls.start(next_line):
break
else:
child_lines.append(next(lines))
next_line = lines.peek()
return tokenize(child_lines), attribute
def __init__(self, match):
self.children, self.attribute = match
+6
View File
@@ -3,6 +3,7 @@ import re
from mistletoe.block_token import BlockToken, tokenize
captures = [
# Website:
"margin",
"indepth",
"webmargin",
@@ -18,6 +19,11 @@ captures = [
"webonly",
"latexonly",
"wordorigin",
# Slides:
"note",
"fragment",
"left",
"right",
]
+15 -7
View File
@@ -1,12 +1,20 @@
beautifulsoup4==4.13.4
coverage==7.6.9
iniconfig==2.0.0
certifi==2025.4.26
charset-normalizer==3.4.2
coverage==7.9.1
idna==3.10
iniconfig==2.1.0
jinja2==3.1.6
joblib==1.5.1
markupsafe==3.0.2
mistletoe==1.4.0
packaging==24.2
pluggy==1.5.0
pytest==8.3.4
pytest-cov==6.0.0
packaging==25.0
pluggy==1.6.0
pygments==2.19.1
pytest==8.4.0
pytest-cov==6.2.1
requests==2.32.3
soupsieve==2.7
typing-extensions==4.13.2
tqdm==4.67.1
typing-extensions==4.14.0
urllib3==2.4.0
+135 -71
View File
@@ -1,5 +1,5 @@
import json
import os
import random
import shutil
from jinja2 import Environment, FileSystemLoader
@@ -8,23 +8,31 @@ from mistletoe import Document
from tqdm import tqdm
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
from .config import Config
class Build:
def __init__(self):
def __init__(self, config: Config):
self.config = config
memory = Memory("./cache", verbose=0)
self.env = Environment(loader=FileSystemLoader("templates/html"))
self.env = Environment(loader=FileSystemLoader("templates/"))
self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers
self.questions = self.__parse_katalog()
# Decorate the method with memory.cache
self.__build_question = memory.cache(self.__build_question)
self.__build_chapter = memory.cache(self.__build_chapter)
self.__build_section = memory.cache(self.__build_section)
self.__build_page = memory.cache(self.__build_page)
self.__build_question_slide = memory.cache(self.__build_question_slide)
self.__build_chapter = memory.cache(self.__build_chapter)
self.__build_section = memory.cache(self.__build_section)
self.__build_page = memory.cache(self.__build_page)
self.__build_chapter_slidedeck = memory.cache(self.__build_chapter_slidedeck)
def __parse_katalog(self):
with open("data/fragenkatalog3b.json") as fragenkatalog_file:
fragenkatalog = json.load(fragenkatalog_file)
with self.config.p_fragenkatalog.open() as file:
fragenkatalog = json.load(file)
questions = {}
@@ -41,13 +49,13 @@ class Build:
return questions
# cached
def __build_question(self, input):
def __build_question(self, input, template_file="html/question.html"):
"""Combines the original question dataset from BNetzA with our internal metadata"""
question_template = self.env.get_template("question.html")
question_template = self.env.get_template(template_file)
with open("data/metadata.json") as metadata_file:
metadata = json.load(metadata_file)
with (self.config.p_data / "metadata.json").open() as file:
metadata = json.load(file)
number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123
question = self.questions[number]
@@ -88,32 +96,29 @@ class Build:
answer_pictures=answer_pictures,
)
def __build_question_slide(self, input):
return self.__build_question(input, template_file="slide/question.html")
# cached
def __build_page(self, content, course_wrapper=False, sidebar=None) :
page_template = self.env.get_template("page.html")
return page_template.render(
content=content,
course_wrapper=course_wrapper,
sidebar=sidebar
)
def __build_page(self, content, course_wrapper=False, sidebar=None):
page_template = self.env.get_template("html/page.html")
return page_template.render(content=content, course_wrapper=course_wrapper, sidebar=sidebar)
def __picture_handler(self, id):
os.makedirs("build/pictures", exist_ok=True)
src = f"data/pictures/{id}.svg"
dst = f"build/pictures/{id}.svg"
shutil.copyfile(src, dst)
self.config.p_build_pictures.mkdir(parents=True, exist_ok=True)
file = f"{id}.svg"
shutil.copyfile(self.config.p_data_pictures / file, self.config.p_build_pictures / file)
def __photo_handler(self, id):
os.makedirs("build/photos", exist_ok=True)
src = f"data/photos/{id}.jpg"
dst = f"build/photos/{id}.jpg"
shutil.copyfile(src, dst)
self.config.p_build_photos.mkdir(parents=True, exist_ok=True)
file = f"{id}.jpg"
shutil.copyfile(self.config.p_data_photos / file, self.config.p_build_photos / file)
# cached
def __build_chapter(self, edition, edition_name, number, chapter, next_chapter=None):
chapter_template = self.env.get_template("chapter.html")
next_chapter_template = self.env.get_template("next_chapter.html")
with open(f'build/{edition}_chapter_{chapter["ident"]}.html', 'w') as file:
chapter_template = self.env.get_template("html/chapter.html")
next_chapter_template = self.env.get_template("html/next_chapter.html")
with (self.config.p_build / f"{edition}_chapter_{chapter['ident']}.html").open("w") as file:
result = chapter_template.render(
edition=edition,
name=edition_name,
@@ -123,7 +128,7 @@ class Build:
if next_chapter is not None:
result += next_chapter_template.render(
url=f"{edition}_chapter_{next_chapter["ident"]}.html",
url=f"{edition}_chapter_{next_chapter['ident']}.html",
title=next_chapter["title"],
)
@@ -131,22 +136,23 @@ class Build:
file.write(result)
def __include_handler(self, include):
with open("data/includes.json") as file:
with (self.config.p_data / "includes.json").open() as file:
includes = json.load(file)
return includes.get(include)
# cached
def __build_section(self, edition, edition_name, section, chapter, next_section=None, next_chapter=None):
section_template = self.env.get_template("section.html")
next_section_template = self.env.get_template("next_section.html")
next_chapter_template = self.env.get_template("next_chapter.html")
with open(f'build/{edition}_{section["ident"]}.html', 'w') as file:
def __build_section(
self, edition, edition_name, section, section_id, chapter, next_section=None, next_chapter=None
):
section_template = self.env.get_template("html/section.html")
next_section_template = self.env.get_template("html/next_section.html")
next_chapter_template = self.env.get_template("html/next_chapter.html")
with (self.config.p_build / f"{edition}_{section['ident']}.html").open("w") as file:
with FiftyOhmHtmlRenderer(
self.__build_question,
self.__picture_handler,
self.__photo_handler,
self.__include_handler
question_renderer=self.__build_question,
picture_handler=self.__picture_handler,
photo_handler=self.__photo_handler,
include_handler=self.__include_handler,
) as renderer:
section["content"] = renderer.render(Document(section["content"]))
@@ -154,56 +160,117 @@ class Build:
edition=edition,
name=edition_name,
section=section,
section_id=section_id,
chapter=chapter,
)
if next_section is not None:
result += next_section_template.render(
url=f"{edition}_{next_section["ident"]}.html",
url=f"{edition}_{next_section['ident']}.html",
title=next_section["title"],
)
elif next_chapter is not None:
result += next_chapter_template.render(
url=f"{edition}_chapter_{next_chapter["ident"]}.html",
url=f"{edition}_chapter_{next_chapter['ident']}.html",
title=next_chapter["title"],
)
result = self.__build_page(result, course_wrapper=True)
file.write(result)
def __build_chapter_slidedeck(self, edition, chapter, sections, next_chapter):
with (self.config.p_build / f"{edition}_slide_{chapter['ident']}.html").open("w") as file:
slide_template = self.env.get_template("slide/slide.html")
help_template = self.env.get_template("slide/help.html")
next_template = self.env.get_template("slide/next.html")
with FiftyOhmHtmlSlideRenderer(
question_renderer=self.__build_question_slide,
picture_handler=self.__picture_handler,
photo_handler=self.__photo_handler,
include_handler=self.__include_handler,
) as renderer:
result = "<section>\n"
result += f'<section data-background="#DAEEFA">\n<h1>{chapter["title"]}</h1>\n</section>\n'
result += help_template.render()
result += "</section>\n"
for section in sections:
if not section["slide"].startswith("---"):
section["slide"] = "---\n" + section["slide"]
tmp = f'<section data-background="#DAEEFA">\n<h1>{section["title"]}</h1>\n</section>\n'
tmp += renderer.render(Document(section["slide"]))
result += f"<section>{tmp}</section>\n"
result += next_template.render(
edition=edition,
next_chapter=next_chapter,
chapter=chapter,
)
result = slide_template.render(content=result)
file.write(result)
def __filter_shuffle_answers(self, seq):
answers = []
firstrun = True
for answer in seq:
if firstrun:
answers.append({"content": answer, "correct": True})
firstrun = False
else:
answers.append({"content": answer, "correct": False})
random.shuffle(answers)
return answers
def __build_book_index(self, book):
template = self.env.get_template("course_index.html")
with open(f"build/{book['edition']}_course_index.html", "w") as file:
template = self.env.get_template("html/course_index.html")
with (self.config.p_build / f"{book['edition']}_course_index.html").open("w") as file:
result = template.render(
book=book,
)
result = self.__build_page(result)
file.write(result)
file.write(result)
def __build_slide_index(self, book):
template = self.env.get_template("slide/slide_index.html")
with (self.config.p_build / f"{book['edition']}_slide_index.html").open("w") as file:
result = template.render(
book=book,
)
result = self.__build_page(result)
file.write(result)
def build_edition(self, edition):
self.config.p_build.mkdir(exist_ok=True)
edition = edition.upper()
with open(f'data/book_{edition}.json') as f:
book = json.load(f)
with (self.config.p_data / f"book_{edition}.json").open() as file:
book = json.load(file)
chapters = book["chapters"]
edition_name = book["title"]
self.__build_book_index(book)
for number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"),1):
self.__build_slide_index(book)
for number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"), 1):
next_chapter = chapters[number] if number < len(chapters) else None
self.__build_chapter(edition, edition_name, number, chapter, next_chapter)
self.__build_chapter_slidedeck(edition, chapter, chapter["sections"], next_chapter)
for i, section in enumerate(chapter["sections"],1) :
for i, section in enumerate(chapter["sections"], 1):
next_section = chapter["sections"][i] if i < len(chapter["sections"]) else None
self.__build_section(edition, edition_name, section, chapter, next_section, next_chapter)
self.__build_section(edition, edition_name, section, i, chapter, next_section, next_chapter)
def build_assets(self):
shutil.copytree("assets", "build/assets", dirs_exist_ok=True)
shutil.copytree(self.config.p_assets, self.config.p_build_assets, dirs_exist_ok=True)
def __parse_snippets(self):
with open("data/snippets.json") as f:
snippets = json.load(f)
with (self.config.p_data / "snippets.json").open() as file:
snippets = json.load(file)
with FiftyOhmHtmlRenderer(self.__build_question, self.__picture_handler, self.__photo_handler) as renderer:
with FiftyOhmHtmlRenderer(
question_renderer=self.__build_question,
picture_handler=self.__picture_handler,
photo_handler=self.__photo_handler,
include_handler=self.__include_handler,
) as renderer:
for key, value in snippets.items():
snippets[key] = renderer.render_inner(Document(value))
# Remove leading <p> and trailing </p>:
@@ -211,35 +278,32 @@ class Build:
return snippets
def __parse_contents(self):
with open("data/content.json") as f:
contents = json.load(f)
with (self.config.p_data / "content.json").open() as file:
contents = json.load(file)
return contents
def __build_index(self, snippets):
template = self.env.get_template("index.html")
template = self.env.get_template("html/index.html")
result = template.render({"snippets": snippets})
with open("build/index.html", "w") as file:
with (self.config.p_build / "index.html").open("w") as file:
result = self.__build_page(result)
file.write(result)
def __build_course_page(self, snippets, template, page):
template = self.env.get_template(f"{template}.html")
result=template.render({"snippets": snippets})
template = self.env.get_template(f"html/{template}.html")
result = template.render({"snippets": snippets})
with open(f"build/{page}.html", "w") as file:
with (self.config.p_build / f"{page}.html").open("w") as file:
result = self.__build_page(result)
file.write(result)
def __build_html_page(self, contents, page):
for content in contents:
if content["url_part"] == page:
with open(f"build/{page}.html", "w") as file:
result = self.__build_page(
content=content["content"],
sidebar=content["sidebar"]
)
with (self.config.p_build / f"{page}.html").open("w") as file:
result = self.__build_page(content=content["content"], sidebar=content["sidebar"])
file.write(result)
def build_website(self):
@@ -250,4 +314,4 @@ class Build:
self.__build_course_page(snippets, "kurse-liste", "kurse_vor_ort_liste")
self.__build_course_page(snippets, "patenkarte", "patenkarte")
self.__build_html_page(contents, "pruefung")
self.__build_html_page(contents, "infos")
self.__build_html_page(contents, "infos")
+19 -7
View File
@@ -1,12 +1,10 @@
import json
import os
from pathlib import Path
class Config:
env_prefix = "OHM"
access_token: str
base_url: str
no_latex: bool
__env_prefix = "OHM"
def __init__(self):
if os.path.isfile("config/config.json"):
@@ -19,14 +17,28 @@ class Config:
self.content_base_url = self.get_config_value("directus_content_base_url", "https://redaktion.50ohm.de/")
self.no_latex = self.get_config_value("no_latex", False)
self.p_fragenkatalog = Path(self.get_config_value("path_fragenkatalog", "./data/fragenkatalog3b.json"))
self.p_data = Path("./data")
self.p_data_photos = self.p_data / "photos"
self.p_data_pictures = self.p_data / "pictures"
self.p_build = Path("./build")
self.p_build_photos = self.p_build / "photos"
self.p_build_pictures = self.p_build / "pictures"
self.p_build_assets = self.p_build / "assets"
self.p_assets = Path("./assets")
def get_config_value(self, key: str, default=None):
if key in self.config:
return self.config[key]
elif f"{self.env_prefix}_{key.upper()}" in os.environ.keys():
return os.environ[f"{self.env_prefix}_{key.upper()}"]
elif f"{self.__env_prefix}_{key.upper()}" in os.environ.keys():
return os.environ[f"{self.__env_prefix}_{key.upper()}"]
elif default is not None:
return default
else:
raise Exception(
f"ERROR: Required value for '{key}' not found. Add to config.json or env as '${self.env_prefix}_{key.upper()}'"
f"ERROR: Required value for '{key}' not found."
f"Add to config.json or env as '${self.__env_prefix}_{key.upper()}'"
)
+34 -23
View File
@@ -6,9 +6,13 @@ import shutil
from joblib import Memory, Parallel, delayed
from tqdm import tqdm
from api.directus import DirectusAPI
from .config import Config
class Download:
def __init__(self, api, content_api, config):
def __init__(self, api: DirectusAPI, content_api: DirectusAPI, config: Config):
self.api = api
self.config = config
self.content_api = content_api
@@ -60,7 +64,7 @@ class Download:
"edition": edition,
}
with open("data/book_" + edition + ".json", "w", encoding="utf-8") as file:
with (self.config.p_data / f"book_{edition}.json").open("w", encoding="utf-8") as file:
json.dump(out_edition, file, ensure_ascii=False, indent=4)
def download_question_metadata(self):
@@ -82,26 +86,26 @@ class Download:
}
# Serializing json:
with open("data/metadata.json", "w") as f:
with (self.config.p_data / "metadata.json").open("w", encoding="utf-8") as f:
json.dump(result, f, indent=4)
f.close()
def download_photos(self):
self.config.p_data_photos.mkdir(parents=True, exist_ok=True)
photos = self.api.get("items/Fotos", params={"limit": -1})
for photo in tqdm(photos, desc="Downloading photos"):
data = self.api.get_file("assets/" + photo["photo"])
with open(f"./data/photos/{photo['id']}.jpg", "wb") as file:
with (self.config.p_data_photos / f"{photo['id']}.jpg").open("wb") as file:
file.write(data)
def __symlink_pictures(self):
os.chdir("./data/pictures")
if not os.path.islink("foto"):
os.system("ln -s ../photos foto")
os.chdir("../..")
def download_pictures(self):
self.__symlink_pictures()
self.config.p_data_pictures.mkdir(parents=True, exist_ok=True)
# Symlink photos, so LaTeX can access these assets to render into graphics.
foto_link = self.config.p_data_pictures / "foto"
if not foto_link.is_symlink():
foto_link.symlink_to(self.config.p_data_photos)
memory = Memory("./cache", verbose=0)
@@ -115,17 +119,22 @@ class Download:
return
if self.config.no_latex:
shutil.copyfile("./assets/images/50ohm_gray.svg", f"./data/pictures/{pid}.svg")
shutil.copyfile("./assets/images/50ohm_gray.svg", self.config.p_data_pictures / f"{pid}.svg")
else:
with open(f"./data/pictures/img/{pid}include.tex", "w") as file:
with (self.config.p_data_pictures / f"img/{pid}include.tex").open("w") as file:
file.write(picture["latex"])
with open(f"./data/pictures/{pid}.tex", "w", encoding="utf-8") as file:
aux_file = self.config.p_data_photos / f"{pid}.tex"
with aux_file.open("w", encoding="utf-8") as file:
# Write the auxilary LaTeX file:
file.write(f"\\documentclass{{BNetzA-Fragenkatalog}}\\DARCimageOnly{{9cm}}{{{pid}include}}")
# Build the picture:
os.system(f"latexmk -lualatex -f -interaction=nonstopmode ./data/pictures/{pid}.tex > /dev/null 2>&1")
os.system(f"latexmk -lualatex -f -interaction=nonstopmode {aux_file} > /dev/null 2>&1")
# Convert the PDF to SVG:
os.system(f"pdftocairo -svg data/pictures/{pid}.pdf - > data/pictures/{pid}.svg")
os.system(
"pdftocairo -svg"
f"{self.config.p_data_pictures / f'{pid}.pdf'} - > {self.config.p_data_pictures / f'{pid}.svg'}"
)
pictures = self.api.get("items/pictures", params={"limit": -1})
@@ -139,20 +148,22 @@ class Download:
result = {}
for include in tqdm(includes, desc="Downloading includes"):
result[include["ident"]] = include["content"]
with open("data/includes.json", "w") as f:
json.dump(result, f, indent=4)
with (self.config.p_data / "includes.json").open("w", encoding="utf-8") as file:
json.dump(result, file, indent=4)
def download_snippets(self):
snippets = {}
for snippet in tqdm(self.content_api.get("items/snippet"), desc="Downloading snippets"):
snippets[snippet["ident"]] = snippet["content"]
with open("data/snippets.json", "w", encoding="utf-8") as file:
with (self.config.p_data / "snippets.json").open("w", encoding="utf-8") as file:
json.dump(snippets, file, ensure_ascii=False, indent=4)
def download_content(self):
contents = []
for content in self.content_api.get("items/content"): #tqdm(self.content_api.get("items/content"), desc="Downloading content"):
for content in self.content_api.get(
"items/content"
): # tqdm(self.content_api.get("items/content"), desc="Downloading content"):
contents.append(
{
"url_part": content["url_part"],
@@ -160,6 +171,6 @@ class Download:
"sidebar": content["sidebar"],
}
)
with open("data/content.json", "w", encoding="utf-8") as file:
with (self.config.p_data / "content.json").open("w", encoding="utf-8") as file:
json.dump(contents, file, ensure_ascii=False, indent=4)
+112
View File
@@ -0,0 +1,112 @@
<section>
<h3>Navigationshilfe</h3>
<p>Diese Navigationshilfe zeigt die ersten Schritte zur Verwendung der Präsention. Sie kann mit &xrarr; (Pfeiltaste
rechts) übersprungen werden.</p>
</section>
<section style="font-size: smaller;">
<h4>Navigation</h4>
<div id="left">
<p>Zwischen den Folien und Abschnitten kann man mittels der Pfeiltasten hin- und herspringen, dazu kann man auch
die Pfeiltasten am Computer nutzen.</p>
<img src="assets/images/slides/navigation.png" alt="Navigationspfeile für die Präsentation" />
</div>
<div id="right">
<ul class="list">
<li>Pfeil runter und hoch: Nächste / Vorherige Folie</li>
<li>Pfeil rechts und links: Nächster / Vorheriger Abschnitt</li>
<li>Leertaste oder „n“: Der Reihe nach alle Elemente in Folien aufdecken oder zur nächsten Folie blättern
</li>
<li>Shift-Leertaste oder „p“: Der Reihe nach Elemente rückwärts zudecken oder zur vorherigen Folie blättern
</li>
</ul>
</div>
</section>
<section style="font-size: smaller;">
<h4>Weitere Funktionen</h4>
<p>Mit ein paar Tastenkürzeln können weitere Funktionen aufgerufen werden. Die wichtigsten sind:</p>
<dl>
<dt>F1</dt>
<dd>Help / Hilfe</dd>
<dt>o</dt>
<dd>Overview / Übersicht aller Folien</dd>
<dt>s</dt>
<dd>Speaker View / Referentenansicht</dd>
<dt>f</dt>
<dd>Full Screen / Vollbildmodus</dd>
<dt>b</dt>
<dd>Break, Black, Pause / Ausblenden der Präsentation</dd>
<dt>Alt-Click</dt>
<dd>In die Folie hin- oder herauszoomen</dd>
</dl>
</section>
<section style="font-size: smaller;">
<h4>Übersicht</h4>
<div id="left">
<p>Die Präsentation ist zweidimensional aufgebaut. Dadurch sind in Spalten die einzelnen Abschnitte eines
Kapitels und in den Reihen die Folien zu den Abschnitten.</p>
<p>Tippt man ein „o“ ein, bekommt man eine Übersicht über alle Folien des jeweiligen Kapitels. Das hilft
sich zunächst einen Überblick zu verschaffen oder sich zu orientieren, wenn man das Gefühlt hat sich
„verlaufen“ zu haben. Die Navigation erfolgt über die Pfeiltasten.</p>
</div>
<div id="right">
<img src="assets/images/slides/overview.png" alt="Übersicht aller Folien" />
<p>Durch Anklicken einer Folie wird diese präsentiert.</p>
</div>
</section>
<section style="font-size: smaller;">
<h4>Referentenansicht</h4>
<div id="left">
<img src="assets/images/slides/notes.png" alt="Referentenansicht" />
<p>Tippt man ein „s“ ein, bekommt man ein neues Fenster, die Referentenansicht.</p>
<p>Indem man „Layout“ auswählt, kann man zwischen verschieden Anordnungen der Elemente auswählen.</p>
</div>
<div id="right">
<p>Die Referentenansicht bietet folgende Elemente:</p>
<ul class="list">
<li>Links sieht man die aktuelle Folie</li>
<li>Rechts oben sieht man die nächste Folie</li>
<li>Rechts in der Mitte Hilfsmittel zur Zeiteinteilung</li>
<li>Rechts unten, die „Notizen für den Vortragenden“</li>
<li>Unten die Pfeile zur Navigation</li>
</ul>
</div>
</section>
<section style="font-size: smaller;">
<h3>Praxistipps zur Referentenansicht</h3>
<ul class="list">
<li>Wenn man mit einem Projektor arbeitet, stellt man im Betriebssystem die Nutzung von 2 Monitoren ein: Die
Referentenansicht wird dann zum Beispiel auf dem Laptop angezeigt, während die Teilnehmer die
Präsentation angezeigt bekommen.</li>
<li>Bei einer Online-Präsentation, wie beispielsweise auf <a href="https://treff.darc.de/">TREFF.darc.de</a>
präsentiert man den Browser-Tab und navigiert im „Speaker View“ Fenster.</li>
<li>Die Referentenansicht bezieht sich immer auf ein Kapitel. Am Ende des Kapitels muss sie geschlossen
werden, um im neuen Kapitel eine neue Referentenansicht zu öffnen.</li>
<li>Um mit dem Mauszeiger etwas zu markieren oder den Zoom zu verwenden, muss mit der Maus auf den
Bildschirm mit der Präsentation gewechselt werden.</li>
</ul>
</section>
<section style="font-size: smaller;">
<h4>Vollbild</h4>
<p>Tippt man ein „f“ ein, wird die aktuelle Folie im Vollbild angezeigt. Mit „Esc“ kann man diesen wieder
verlassen.</p>
<p>Das ist insbesondere für den Bildschirm mit der Präsentation für das Publikum praktisch.</li>
</section>
<section style="font-size: smaller;">
<h4>Ausblenden</h4>
<p>Tippt man ein „b“ ein, wird die Präsentation ausgeblendet.</p>
<p>Sie kann wie folgte wieder eingeblendet werden:</p>
<ul class="list">
<li>Durch klicken in das Fenster.</li>
<li>Durch nochmaliges Drücken von „b“.</li>
<li>Durch klicken der Schaltfläche „Resume presentation:</li>
</ul>
<img src="assets/images/slides/resume.png" alt="Schaltfläche für Resume Presentation" />
</section>
<section style="font-size: smaller;">
<h4>Zoom</h4>
<p>Bei gedrückter Alt-Taste und einem Mausklick in der Präsentation wird in diesen Teil hineingezoomt. Das ist
praktisch, um Details von Schaltungen hervorzuheben. Durh einen nochmaligen Mausklick zusammen mit Alt wird
wieder herausgezoomt.</p>
<p>Das Zoomen funktioniert nur im ausgewählten Fenster. Die Referentenansicht ist hier nicht mit dem
Präsenationsansicht gesynct.</p>
</section>
+15
View File
@@ -0,0 +1,15 @@
<section data-background="#DAEEFA">
<h2>Fragen?</h2>
<hr />
<h3>Links zu diesem Foliensatz</h3>
<ul>
{% if next_chapter is defined -%}
<li><a href="{{ edition }}_slide_{{ next_chapter["ident"] }}.html">Weiter zum nächsten Kapitel: {{
next_chapter["title"] }}</a></li>
{% endif -%}
<li><a href="{{ edition }}_chapter_{{ chapter["ident"] }}.html">Zum Kapitel auf 50Ohm.de</a></li>
<li><a href="{{ edition }}_slide_index.html">Zur Kapitelübersicht aller Foliensätze</a></li>
<li><a href="?print-pdf">Diesen Foliensatz zum Ausdrucken öffnen</a></li>
<li><a href="?print-pdf&showNotes=true">Diesen Foliensatz mit Notizen zum Ausdrucken öffnen</a></li>
</ul>
</section>
+24
View File
@@ -0,0 +1,24 @@
{% set answer_label = ["A", "B", "C", "D"] -%}
{% if picture_question or (answer_pictures[0] and layout in ["1", "9", None]) -%}
{# OHM-13: Fix layout for 4x times picture in row to 2x2 field #}
<div class="question r-fit-text layout-8">
{% else -%}
<div class="question r-fit-text layout-{{ layout }}">
{% endif -%}
<div class="question-text">{{ number }}: {{ question }}</div>
{% if picture_question -%}
<div class="question-picture"><img class="r-stretch" src="pictures/{{ picture_question }}.svg"></div>
{% endif -%}
<div class="answers">
{% if answers[0] -%}
{% for answer in answers|shuffle_answers -%}
<p{% if not answer.correct %} class="fragment semi-fade-out" data-fragment-index="1" {% endif -%}><b>{{ answer_label[loop.index - 1] }}:</b> {{ answer.content }}</p>
{% endfor -%}
{% endif -%}
{% if answer_pictures[0] -%}
{% for answer in answer_pictures|shuffle_answers -%}
<div class="question-picture{% if not answer.correct %} fragment semi-fade-out{% endif -%}" {% if not answer.correct -%} data-fragment-index="1" {% endif -%}><b>{{ answer_label[loop.index - 1] }}:</b><img class="r-stretch" src="pictures/{{ answer.content }}.svg"></div>
{% endfor -%}
{% endif -%}
</div>
</div>
+56
View File
@@ -0,0 +1,56 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>DARC Ausbildungsplattform</title>
<meta name="description" content="">
<meta name="author" content="">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="assets/reveal.js/dist/reset.css">
<link rel="stylesheet" href="assets/reveal.js/dist/reveal.css">
<link rel="stylesheet" href="assets/style-slide.css">
<!-- Theme used for syntax highlighting of code -->
<link rel="stylesheet" href="assets/reveal.js/plugin/highlight/monokai.css">
<!-- Chart plugin -->
<script src="assets/reveal.js-plugins/chart/plugin.js"></script>
<script src="assets/chartjs/4.4.1/chart.min.js"></script>
</head>
<body>
<div class="reveal">
<!-- Any section element inside of this container is displayed as a slide -->
<div class="slides">
{{ content }}
</div>
<script src="assets/reveal.js/dist/reveal.js"></script>
<script src="assets/reveal.js/plugin/zoom/zoom.js"></script>
<script src="assets/reveal.js/plugin/notes/notes.js"></script>
<script src="assets/reveal.js/plugin/search/search.js"></script>
<script src="assets/reveal.js/plugin/markdown/markdown.js"></script>
<script src="assets/reveal.js/plugin/highlight/highlight.js"></script>
<script src="assets/reveal.js/plugin/math/math.js"></script>
<script>
// Also available as an ES module, see:
// https://revealjs.com/initialization/
Reveal.initialize({
controls: true,
progress: true,
slideNumber: 'c/t',
showSlideNumber: 'speaker',
center: true,
hash: true,
transition: 'slide',
showNotes: false,
pdfSeparateFragments: false,
// Learn about plugins: https://revealjs.com/plugins/
plugins: [ RevealZoom, RevealNotes, RevealSearch, RevealMarkdown, RevealHighlight, RevealMath.KaTeX, RevealChart ]
});
</script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
<h1>Folien: {{ book["title"] }}</h1>
<p>{{ book["abstract"] }}</p>
<div class="chapter-toc d-grid gap-2">
{% for chapter in book["chapters"] %}
<a class="btn btn-outline-primary btn-labeled" href="{{ book["edition"] }}_slide_{{ chapter["ident"] }}.html">
<span class="btn-label">{{loop.index}}</span>{{ chapter["title"]}}
</a>
{% endfor %}
</div>
+137
View File
@@ -0,0 +1,137 @@
---
# Akzeptanztest
--- style="font-size: 4pt;"
## Kleinere Schrift
* Längere
* Liste
---
## Folie mit direktem Übergang
Mit Text
--- data-transition="none"
## Folie mit direktem Übergang
Mit anderem Text
---
## Externer Link und Formel
[Link zum DARC](https://darc.de/)
$\lambda[[\text{m}]] = \dfrac{300}{f[[\text{MHz}]]} = \dfrac{300}{145,3 \ \text{MHz}} \approx 2,06 \ \text{m}$
---
## Notes Test
Diese Folie hat Notizen
<note>
Die Notiz wird nur in der Speaker-Ansicht angezeigt
* Listen sind auch möglich
* Am besten <u>kurz</u> halten
* Der Platz ist *begrenzt*
</note>
---
## Fragment Test
Dies ist ein Text der direkt da ist
<fragment>
Dieser Text erscheint erst später.
</fragment>
---
## Slide mit QSO
<qso>
Ist diese Frequenz frei? DL1PZ
> *(keine Antwort)*
Ist diese Frequenz frei? DL1PZ
> *(keine Antwort)*
CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
> DL1PZ hier ist DL9MJ bitte kommen
</qso>
---
## Slide mit Picture
[picture:123:p_test:testbild]
---
## Slide mit Photo
[photo:123:n_rst_s-meter:Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt]
---
[question:2482]
---
## Slide Tabelle
| l: Wert | l: Bereich | l: Bedeutung | l: Englisch |
| R | 1 - 5 | Lesbarkeit | Readability |
| S | 1 - 9 | Signalstärke | Signal Strength |
| T | 1 - 9 | Tonqualität | Tone |
[table:n_rst:Die Bestandteile des RST-Rapports]
---
## Slide Mit Markdown
Das ist ein *Text*
* Das ist ein Bullet
* Das ist noch ein Bullet
Hier wird <u>text</u> unterstrichen
--- unit
## Slide Unit
Slide C
--- data-background="#FF00FF"
## Slide PINK
PINK
## Iframe als Hintergrund
--- data-background-iframe="https://f5len.org/tools/locator/" data-background-interactive
---
## Double Column
<left>
[photo:167:rufzeichen_flugzeug:Flugzeug mit dem Rufzeichen DEBPF]
</left>
<right>
* Funkstationen verwenden Rufzeichen, um sich zu identifizieren
* Folge von Buchstaben und Ziffern
* Jedes mit Funk ausgerüstete Flugzeug und Schiff hat ein Rufzeichen
</right>
---
## Folie mit direktem Übergang
Mit Text
--- data-transition="none"
## Folie mit direktem Übergang
Mit anderem Text ... keine ahnung ob das je ging?
+98
View File
@@ -0,0 +1,98 @@
import json
import random
from jinja2 import Environment, FileSystemLoader
from mistletoe import Document
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
def parse_katalog():
with open("data/fragenkatalog3b.json") as fragenkatalog_file:
fragenkatalog = json.load(fragenkatalog_file)
questions = {}
for exampart in fragenkatalog["sections"]:
for chapter in exampart["sections"]:
if "questions" in chapter:
for question in chapter["questions"]:
questions[question["number"]] = question
if "sections" in chapter:
for section in chapter["sections"]:
for question in section["questions"]:
questions[question["number"]] = question
return questions
def filter_shuffle_answers(seq):
try:
answers = []
firstrun = True
for answer in seq:
if firstrun:
answers.append({"content": answer, "correct": True})
firstrun = False
else:
answers.append({"content": answer, "correct": False})
random.shuffle(answers)
return answers
except Exception:
return seq
def question_stub(input):
"""Combines the original question dataset from BNetzA with our internal metadata"""
with open("data/metadata.json") as metadata_file:
metadata = json.load(metadata_file)
number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123
question = questions[number]
metadata = metadata[f"{input}"]
if "answer_a" in question:
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
else:
answers = []
if metadata["picture_a"] != "":
answer_pictures = [
metadata["picture_a"],
metadata["picture_b"],
metadata["picture_c"],
metadata["picture_d"],
]
else:
answer_pictures = []
if "picture_question" in question:
picture_question = metadata["picture_question"]
else:
picture_question = ""
return question_template.render(
question=question["question"],
number=number,
layout=metadata["layout"],
picture_question=picture_question,
answers=answers,
answer_pictures=answer_pictures,
)
env = Environment(loader=FileSystemLoader("templates/"))
env.filters["shuffle_answers"] = filter_shuffle_answers
question_template = env.get_template("slide/question.html")
slide_template = env.get_template("slide/slide.html")
questions = parse_katalog()
def test_html_slides(capsys):
with capsys.disabled():
with open("test/acceptanceTestSlides.md") as file:
content = file.read()
with FiftyOhmHtmlSlideRenderer(question_renderer=question_stub) as renderer:
output = renderer.render(Document(content))
with open("test/acceptanceTestSlides.html", "w") as output_file:
output_file.write(output)
+12 -1
View File
@@ -2,10 +2,11 @@ import mistletoe
import pytest
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
@pytest.mark.html
def test_table_html():
def test_qso_html():
assertions = {
"<qso>\nfoo\n</qso>": FiftyOhmHtmlRenderer.render_tag_helper("qso", '<div class="qso_own">foo</div>\n', 1, 0)
+ "\n",
@@ -21,3 +22,13 @@ def test_table_html():
for assertion in assertions:
assert mistletoe.markdown(assertion, FiftyOhmHtmlRenderer) == assertions[assertion]
@pytest.mark.slide
def test_qso_slide():
assertions = {
"<qso>\nfoo\n> bar\n</qso>":
'<div class="qso r-fit-text">\n<div class="qso_own fragment fade-right">foo</div>\n<div class="qso_other fragment fade-left">bar</div>\n</div>\n\n',
}
for assertion in assertions:
assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion]
+2 -2
View File
@@ -20,7 +20,7 @@ def test_question_html():
def test_function(input):
return f"{input}"
with FiftyOhmHtmlRenderer(test_function) as renderer:
with FiftyOhmHtmlRenderer(question_renderer=test_function) as renderer:
for assertion in assertions:
assert renderer.render(Document(assertion)) == assertions[assertion]
@@ -38,7 +38,7 @@ def test_question_translation_html():
metadata = json.load(file)
return metadata[f"{input}"].get("number")
with FiftyOhmHtmlRenderer(test_function) as renderer:
with FiftyOhmHtmlRenderer(question_renderer=test_function) as renderer:
for assertion in assertions:
assert renderer.render(Document(assertion)) == assertions[assertion]
+30 -1
View File
@@ -2,6 +2,7 @@ import mistletoe
import pytest
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
from renderer.fifty_ohm_latex_renderer import FiftyOhmLaTeXRenderer
@@ -29,7 +30,8 @@ def test_tag_html():
+ "\n",
"<unit>\nFoo\n</unit>": FiftyOhmHtmlRenderer.render_tag_helper("unit", "<p>Foo</p>", 1, 0) + "\n",
"<danger>\nFoo\n</danger>": FiftyOhmHtmlRenderer.render_tag_helper("danger", "<p>Foo</p>", 1, 0) + "\n",
"<wordorigin>\nFoo\n</wordorigin>": FiftyOhmHtmlRenderer.render_tag_helper("wordorigin", "<p>Foo</p>", 1, 0) + "\n",
"<wordorigin>\nFoo\n</wordorigin>": FiftyOhmHtmlRenderer.render_tag_helper("wordorigin", "<p>Foo</p>", 1, 0)
+ "\n",
"<webonly>\nFoo\n</webonly>": "<p>Foo</p>\n",
"<latexonly>\nFoo\n</latexonly>": "",
}
@@ -103,3 +105,30 @@ def test_thematic_break_and_tag_latex():
for assertion in assertions:
assert mistletoe.markdown(assertion, FiftyOhmLaTeXRenderer) == assertions[assertion]
@pytest.mark.slide
def test_tag_slide():
assertions = {
"<note>\nFoo\n</note>": '<aside class="notes">\n<p>Foo</p>\n</aside>\n\n',
"<fragment>\nFoo\n</fragment>": '<div class="fragment">\n<p>Foo</p>\n</div>\n\n',
"<left>\nFoo\n</left>": '<div id="left">\n<p>Foo</p>\n</div>\n\n',
"<right>\nFoo\n</right>": '<div id="right">\n<p>Foo</p>\n</div>\n\n',
}
for assertion in assertions:
assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion]
@pytest.mark.slide
def test_slide_break():
assertions = {
"---\na\n": "<section>\n<p>a</p>\n</section>\n\n",
"---\na\n---\nb\n": "<section>\n<p>a</p>\n</section>\n\n<section>\n<p>b</p>\n</section>\n\n",
"--- foo\na\n": "<section foo>\n<p>a</p>\n</section>\n\n",
"--- foo bar\na\n": "<section foo bar>\n<p>a</p>\n</section>\n\n",
"--- foo bar \na\n": "<section foo bar>\n<p>a</p>\n</section>\n\n",
}
for assertion in assertions:
assert mistletoe.markdown(assertion, FiftyOhmHtmlSlideRenderer) == assertions[assertion]