Merge branch 'main' into feat/improve-reference-counting
This commit is contained in:
-11
@@ -1,11 +0,0 @@
|
|||||||
kind: pipeline
|
|
||||||
type: docker
|
|
||||||
name: default
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: 50ohm.de
|
|
||||||
image: ghcr.io/astral-sh/uv:python3.12-bookworm
|
|
||||||
environment:
|
|
||||||
commands:
|
|
||||||
- uv pip install --system --break-system-packages --requirements requirements.txt
|
|
||||||
- pytest --junitxml=./test-reports/junit.xml
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Pytest Report
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Pytest]
|
||||||
|
types: [completed]
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
checks: write
|
||||||
|
pull-requests: write
|
||||||
|
jobs:
|
||||||
|
checks:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Download Test Report
|
||||||
|
uses: actions/download-artifact@v6
|
||||||
|
with:
|
||||||
|
name: pytest-report
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
- name: Publish Test Report
|
||||||
|
uses: mikepenz/action-junit-report@v6
|
||||||
|
with:
|
||||||
|
commit: ${{github.event.workflow_run.head_sha}}
|
||||||
|
report_paths: pytest.xml
|
||||||
|
comment: true
|
||||||
|
pr_id: ${{ github.event.workflow_run.pull_requests[0].number }}
|
||||||
@@ -2,8 +2,6 @@ name: Pytest
|
|||||||
on: pull_request
|
on: pull_request
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
checks: write
|
|
||||||
pull-requests: write
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
@@ -21,12 +19,10 @@ jobs:
|
|||||||
- name: Install failure annotation plugin
|
- name: Install failure annotation plugin
|
||||||
run: pip install pytest-github-actions-annotate-failures
|
run: pip install pytest-github-actions-annotate-failures
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: pytest --junitxml=pytest.xml
|
||||||
set -o pipefail
|
- name: Upload Test Report
|
||||||
pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov-report=html --cov | tee pytest-coverage.txt
|
uses: actions/upload-artifact@v6
|
||||||
- name: Comment pytest code coverage
|
|
||||||
uses: MishaKav/pytest-coverage-comment@main
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
with:
|
||||||
pytest-coverage-path: ./pytest-coverage.txt
|
name: pytest-report
|
||||||
junitxml-path: ./pytest.xml
|
path: pytest.xml
|
||||||
|
retention-days: 1
|
||||||
@@ -11,6 +11,7 @@ jobs:
|
|||||||
- uses: astral-sh/ruff-action@v3
|
- uses: astral-sh/ruff-action@v3
|
||||||
with:
|
with:
|
||||||
args: check --output-format=github
|
args: check --output-format=github
|
||||||
|
version: "0.15.12"
|
||||||
Format:
|
Format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
@@ -18,3 +19,4 @@ jobs:
|
|||||||
- uses: astral-sh/ruff-action@v3
|
- uses: astral-sh/ruff-action@v3
|
||||||
with:
|
with:
|
||||||
args: format --check --diff
|
args: format --check --diff
|
||||||
|
version: "0.15.12"
|
||||||
|
|||||||
+1
-4
@@ -167,7 +167,4 @@ test/acceptanceTestSlides.html
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.envrc
|
.envrc
|
||||||
config/config.json
|
config/config.json
|
||||||
|
tmp/
|
||||||
data/*
|
|
||||||
!data/fragenkatalog3b.json
|
|
||||||
!data/README.txt
|
|
||||||
@@ -2,28 +2,25 @@
|
|||||||
|
|
||||||
50ohm.de ist die Ausbildungsplattform des DARC e.V.
|
50ohm.de ist die Ausbildungsplattform des DARC e.V.
|
||||||
|
|
||||||
Dieses Repository enthält den Generator für die Seite [50ohm.de](https://50ohm.de), der mit einer Directus-Datenbank als Quelle arbeitet.
|
Dieses Repository enthält den Generator für die Seite [50ohm.de](https://50ohm.de), der mit einem andern [Repository](https://github.com/DARC-e-V/50ohm-contents-dl/) als Quelle für die Inhalte arbeitet.
|
||||||
|
|
||||||
## Überblick
|
## Überblick
|
||||||
|
|
||||||
Die Ausbildungsmaterialien von 50ohm.de werden in einem erweiterten Marcdownformat geschrieben, intern als Darcdown bezeichnet. Kern des Generators ist ein Parser, der auf [mistletoe](https://github.com/miyuchina/mistletoe) basiert und um zusätzliche Syntax und Ausgabeformate erweitert wurde.
|
Die Ausbildungsmaterialien von 50ohm.de werden in einem erweiterten Markdownformat geschrieben, intern als DARCdown bezeichnet. Kern des Generators ist ein Parser, der auf [mistletoe](https://github.com/miyuchina/mistletoe) basiert und um zusätzliche Syntax und Ausgabeformate erweitert wurde.
|
||||||
|
|
||||||
Die möglichen Ausgabeformate sind:
|
Die möglichen Ausgabeformate sind:
|
||||||
- HTML: [`fifty_ohm_html_renderer.py`](renderer/fifty_ohm_html_renderer.py)
|
- HTML: [`fifty_ohm_html_renderer.py`](renderer/fifty_ohm_html_renderer.py)
|
||||||
- LaTeX: [`fifty_ohm_latex_renderer`](renderer/fifty_ohm_latex_renderer.py)
|
- LaTeX: [`fifty_ohm_latex_renderer`](renderer/fifty_ohm_latex_renderer.py)
|
||||||
- HTML für Slides mit reveal.js: [`fifty_ohm_html_slide_renderer.py`](renderer/fifty_ohm_html_slide_renderer.py)
|
- HTML für Slides mit reveal.js: [`fifty_ohm_html_slide_renderer.py`](renderer/fifty_ohm_html_slide_renderer.py)
|
||||||
|
|
||||||
Prinzipiell lassen sich diese Generatoren auch losgelöst vom Directus einsetzen. Der zweite Teil des Generators liegt in `src`, unterteilt in die Prozessschritte Herunderladen ([`download.py`](src/download.py)) und Seite Bauen ([`build.py`](src/build.py)).
|
Prinzipiell lassen sich diese Generatoren auch für andere Quell-Repositories verwenden. Dies ist insbesondere für andere IARU-Verbände interessant, welche auch 50ohm als Ausbildungsseite implementieren wollen. Der eigentliche Teil des Generators liegt in `src`, insbesondere das Skript ([`build.py`](src/build.py)). In dieser Datei finden sich außerdem weitere Begleitfunktionen zum Renderprozess, z.B. das Übersetzen von Fragennummern in Fragen oder das Kopieren von Assets.
|
||||||
|
|
||||||
Hier werden alle Inhalte aus der Datenbank heruntergeladen und strukturiert in verschiedenen JSON-Dateien abgelegt. Diese dienen im Build-Prozess als Quelle für Abschnitte, Kapitelstruktur und weitere Inhalte.
|
## Mitmachen und Kontakt
|
||||||
|
|
||||||
In der `build.py` finden sich außerdem weitere Begleitfunktionen zum Renderprozess, z.B. das Übersetzen von Fragennummern in Fragen oder das Kopieren von Assets.
|
|
||||||
|
|
||||||
## Mitmachen
|
|
||||||
|
|
||||||
Aktuell wird 50ohm durch ein kleines ehrenamtliches Entwicklerteam beim DARC e.V. betreut und weiterentwickelt. Falls du einen Fehler oder einen Funktionswunsch hast, schreibe gerne ein Issue. Auch über einen PR freuen wir uns!
|
Aktuell wird 50ohm durch ein kleines ehrenamtliches Entwicklerteam beim DARC e.V. betreut und weiterentwickelt. Falls du einen Fehler oder einen Funktionswunsch hast, schreibe gerne ein Issue. Auch über einen PR freuen wir uns!
|
||||||
|
Wenn du inhaltliche Beiträge hast, kannst du diese über dieses [Repository](https://github.com/DARC-e-V/50ohm-contents-dl) einreichen.
|
||||||
|
|
||||||
Wenn du inhaltliche Beiträge hast, wende dich bitte an das 50ohm-Autorenteam.
|
Bei Anfragen bitte die Email-Adresse [[email protected]](mailto:[email protected]) nutzen. Weiterhin findet man viele der Beitragenden im Matrix-Kanal [50ohm Contribution](https://matrix.to/#/#50ohm-contribution:darc.de).
|
||||||
|
|
||||||
### Python einrichten und Abhängigkeiten installieren
|
### Python einrichten und Abhängigkeiten installieren
|
||||||
|
|
||||||
@@ -44,7 +41,7 @@ $ uv pip sync requirements.txt
|
|||||||
|
|
||||||
### Ausführen
|
### Ausführen
|
||||||
|
|
||||||
Um 50ohm ausführen zu können, wird in den meisten Fällen ein Zugang zu passenden Directus-Instanzen benötigt. Diese können über eine `config/config.json` konfiguriert werden.
|
Um 50ohm ausführen zu können, muss der Pfad zu einem lokal ausgechecktem Content-Repository in die `config/config.json` eingetragen werden. Ein Beispiel-Content-Repository für deutsche Inhalte findet sich hier: [50ohm-contents-dl](https://github.com/DARC-e-V/50ohm-contents-dl).
|
||||||
|
|
||||||
Die 50ohm.de-Website wird mit folgendem Befehl vollständig gebaut:
|
Die 50ohm.de-Website wird mit folgendem Befehl vollständig gebaut:
|
||||||
|
|
||||||
@@ -52,4 +49,15 @@ Die 50ohm.de-Website wird mit folgendem Befehl vollständig gebaut:
|
|||||||
$ python3 ./build.py
|
$ python3 ./build.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Anschließend ist der Einstiegspunkt in `build/index.html` zu finden.
|
### Starten
|
||||||
|
Der Einstiegspunkt ist in `build/index.html` zu finden.
|
||||||
|
Alle Seiten sind auch in `build/build.zip` gepackt enthalten.
|
||||||
|
|
||||||
|
Um auch die Suchfunktion auf der Seite nutzen zu können, muss ein lokaler Webserver gestartet werden:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ cd build/
|
||||||
|
$ python3 -m http.server 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Anschließend kann die Webseite unter [`http://localhost:8000/index.html`](http://localhost:8000/index.html) aufgerufen werden.
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
|
|
||||||
class DirectusAPI:
|
|
||||||
def __init__(self, base_url, access_token):
|
|
||||||
self.base_url = base_url
|
|
||||||
self.access_token = access_token
|
|
||||||
|
|
||||||
def __get_raw(self, endpoint, params=None):
|
|
||||||
if params is None:
|
|
||||||
params = {}
|
|
||||||
for k in params.keys():
|
|
||||||
if type(params[k]) is dict:
|
|
||||||
params[k] = json.dumps(params[k])
|
|
||||||
url = self.base_url + endpoint
|
|
||||||
result = requests.get(
|
|
||||||
url, params=params, headers={"Authorization": f"Bearer {self.access_token}", "Cache-Control": "no-store"}
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get(self, endpoint, params=None):
|
|
||||||
if params is None:
|
|
||||||
params = {}
|
|
||||||
result = self.__get_raw(endpoint, params)
|
|
||||||
try:
|
|
||||||
result_json = result.json()
|
|
||||||
if "errors" in result_json.keys():
|
|
||||||
print(result_json["errors"])
|
|
||||||
raise Exception("Error communicating with API")
|
|
||||||
data = result_json["data"]
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error: {e}")
|
|
||||||
return None
|
|
||||||
return data
|
|
||||||
|
|
||||||
def get_one(self, endpoint, params=None):
|
|
||||||
if params is None:
|
|
||||||
params = {}
|
|
||||||
results = self.get(endpoint, params)
|
|
||||||
if len(results) == 1:
|
|
||||||
return results[0]
|
|
||||||
elif len(results) == 0:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
raise Exception("One result expected, but got more than one result")
|
|
||||||
|
|
||||||
def get_file(self, endpoint, params=None):
|
|
||||||
if params is None:
|
|
||||||
params = {}
|
|
||||||
result = self.__get_raw(endpoint, params)
|
|
||||||
data = result.content
|
|
||||||
if data is None:
|
|
||||||
raise Exception("No data from API")
|
|
||||||
return data
|
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
|
||||||
|
function formatThousands(input) {
|
||||||
|
|
||||||
|
let s = String(input).trim();
|
||||||
|
|
||||||
|
// Extract exponent (e or E) and convert to \cdot 10^{...}
|
||||||
|
let exponent = "";
|
||||||
|
const expMatch = s.match(/([eE])([+-]?\d+)$/);
|
||||||
|
if (expMatch) {
|
||||||
|
let expValue = expMatch[2];
|
||||||
|
|
||||||
|
// Remove leading "+"
|
||||||
|
if (expValue[0] === "+") {
|
||||||
|
expValue = expValue.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse exponent as a numeric value
|
||||||
|
const expNum = parseInt(expValue, 10);
|
||||||
|
|
||||||
|
// Suppress exponent if it is 0 (no \cdot 10^0 shown)
|
||||||
|
if (expNum !== 0) {
|
||||||
|
exponent = "\\cdot 10^{" + expNum + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s.slice(0, -expMatch[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign (+, -, \pm). Also accept "\\pm" (double-escaped backslash) and optional whitespace.
|
||||||
|
let sign = "";
|
||||||
|
const pmMatch = s.match(/^(?:\\\\pm|\\pm)\s*/);
|
||||||
|
if (pmMatch) {
|
||||||
|
sign = "\\pm";
|
||||||
|
s = s.slice(pmMatch[0].length);
|
||||||
|
} else if (s[0] === "+" || s[0] === "-") {
|
||||||
|
sign = s[0];
|
||||||
|
s = s.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find decimal point (last . or ,)
|
||||||
|
let intPart = s;
|
||||||
|
let fracSep = "";
|
||||||
|
let fracDigits = "";
|
||||||
|
const decMatch = s.match(/([.,])(\d+)$/);
|
||||||
|
if (decMatch) {
|
||||||
|
intPart = s.slice(0, decMatch.index);
|
||||||
|
fracSep = decMatch[1];
|
||||||
|
fracDigits = decMatch[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thousand grouping in integer part
|
||||||
|
if (intPart.length >= 5) {
|
||||||
|
intPart = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, "\\,");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thousand grouping in fractional part (from left in 3-digit blocks)
|
||||||
|
if (fracDigits.length >= 4) {
|
||||||
|
fracDigits = fracDigits.replace(/(\d{3})(?=\d)/g, "$1\\,");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reassemble
|
||||||
|
return sign + intPart + (fracDigits ? (fracSep + fracDigits) : "") + exponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var macros = {
|
||||||
|
",": (context) => context.future()?.text === " " ? "{\\char`,}" : "\\char`,",
|
||||||
|
"\\num": (context) => {
|
||||||
|
const [arg] = context.consumeArgs(1);
|
||||||
|
const raw = arg.reverse().map(t => t.text).join("");
|
||||||
|
return formatThousands(raw);
|
||||||
|
},
|
||||||
|
"\\qty": (context) => {
|
||||||
|
const [numTok, unitTok] = context.consumeArgs(2);
|
||||||
|
const rawNum = numTok.reverse().map(t => t.text).join("");
|
||||||
|
const unit = unitTok.reverse().map(t => t.text).join("");
|
||||||
|
const formattedNum = formatThousands(rawNum);
|
||||||
|
if (unit.startsWith("\\degree")) {
|
||||||
|
return formattedNum + "\\mathrm{" + unit + "}";
|
||||||
|
} else {
|
||||||
|
return formattedNum + "\\,\\mathrm{" + unit + "}";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"\\qtyrange": (context) => {
|
||||||
|
const [aTok, bTok, unitTok] = context.consumeArgs(3);
|
||||||
|
const aRaw = aTok.reverse().map(t => t.text).join("");
|
||||||
|
const bRaw = bTok.reverse().map(t => t.text).join("");
|
||||||
|
const unit = unitTok.reverse().map(t => t.text).join("");
|
||||||
|
const aFmt = formatThousands(aRaw);
|
||||||
|
const bFmt = formatThousands(bRaw);
|
||||||
|
if (unit.startsWith("\\degree")) {
|
||||||
|
return aFmt + "\\text{--}\\," + bFmt + "\\mathrm{" + unit + "}";
|
||||||
|
} else {
|
||||||
|
return aFmt + "\\,\\text{--}\\," + bFmt + "\\,\\mathrm{" + unit + "}";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"\\unit": "{\\mathrm{#1}}",
|
||||||
|
"\\squared": "{^{2}}",
|
||||||
|
"\\cubed": "{^{3}}",
|
||||||
|
"\\per": "/",
|
||||||
|
"\\percent": "\\%",
|
||||||
|
"\\ppm": "\\text{ppm}",
|
||||||
|
"\\tera": "\\text{T}",
|
||||||
|
"\\giga": "\\text{G}",
|
||||||
|
"\\mega": "\\text{M}",
|
||||||
|
"\\kilo": "\\text{k}",
|
||||||
|
"\\dezi": "\\text{d}",
|
||||||
|
"\\centi": "\\text{c}",
|
||||||
|
"\\milli": "\\text{m}",
|
||||||
|
"\\micro": "\\text{μ}",
|
||||||
|
"\\nano": "\\text{n}",
|
||||||
|
"\\pico": "\\text{p}",
|
||||||
|
"\\kilogram": "\\text{kg}",
|
||||||
|
"\\gram": "\\text{g}",
|
||||||
|
"\\meter": "\\text{m}",
|
||||||
|
"\\second": "\\text{s}",
|
||||||
|
"\\hour": "\\text{h}",
|
||||||
|
"\\ampere": "\\text{A}",
|
||||||
|
"\\kelvin": "\\text{K}",
|
||||||
|
"\\mol": "\\text{mol}",
|
||||||
|
"\\bel": "\\text{B}",
|
||||||
|
"\\dezibel": "\\text{dB}",
|
||||||
|
"\\dB": "\\text{dB}",
|
||||||
|
"\\dBm": "\\text{dBm}",
|
||||||
|
"\\dBu": "\\text{dBu}",
|
||||||
|
"\\dBV": "\\text{dB}\\volt",
|
||||||
|
"\\dBuV": "\\text{dB}\\micro\\volt",
|
||||||
|
"\\dBW": "\\text{dBW}",
|
||||||
|
"\\dBi": "\\text{dBi}",
|
||||||
|
"\\dBd": "\\text{dBd}",
|
||||||
|
"\\candela": "\\text{cd}",
|
||||||
|
"\\newton": "\\text{N}",
|
||||||
|
"\\hertz": "\\text{Hz}",
|
||||||
|
"\\pascal": "\\text{Pa}",
|
||||||
|
"\\volt": "\\text{V}",
|
||||||
|
"\\watt": "\\text{W}",
|
||||||
|
"\\joule": "\\text{J}",
|
||||||
|
"\\henry": "\\text{H}",
|
||||||
|
"\\farad": "\\text{F}",
|
||||||
|
"\\coulomb": "\\text{C}",
|
||||||
|
"\\ohm": "\\Omega",
|
||||||
|
"\\Ohm": "\\Omega",
|
||||||
|
"\\weber": "\\text{Wb}",
|
||||||
|
"\\tesla": "\\text{T}",
|
||||||
|
"\\bit": "\\text{Bit}",
|
||||||
|
"\\baud": "\\text{Bd}",
|
||||||
|
"\\degreeCelsius": "\\,\\degree\\text{C}",
|
||||||
|
"\\liter": "\\text{l}",
|
||||||
|
"\\oszidiv": "\\text{div}",
|
||||||
|
"\\sps": "\\text{sps}",
|
||||||
|
};
|
||||||
|
|
||||||
|
renderMathInElement(document.body, {
|
||||||
|
delimiters: [
|
||||||
|
{ left: '$$', right: '$$', display: true }, // Note: $$ has to come before $
|
||||||
|
{ left: '$', right: '$', display: false },
|
||||||
|
{ left: '\\[', right: '\\]', display: true },
|
||||||
|
{ left: "\\(", right: "\\)", display: false },
|
||||||
|
],
|
||||||
|
throwOnError: false,
|
||||||
|
macros: macros
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
+71
-64
@@ -1,65 +1,3 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function() {
|
|
||||||
|
|
||||||
var macros = {
|
|
||||||
",": (context) => context.future()?.text === " " ? "{\\char`,}" : "\\char`,",
|
|
||||||
"\\qty": "{#1\\,\\mathrm{#2}}",
|
|
||||||
"\\qtyrange": "{#1\\text{--}#2\\,\\mathrm{#3}}",
|
|
||||||
"\\unit": "{\\mathrm{#1}}",
|
|
||||||
"\\squared": "{^{2}}",
|
|
||||||
"\\cubed": "{^{3}}",
|
|
||||||
"\\per": "/",
|
|
||||||
"\\percent": "\\%",
|
|
||||||
"\\tera": "\\text{T}",
|
|
||||||
"\\giga": "\\text{G}",
|
|
||||||
"\\mega": "\\text{M}",
|
|
||||||
"\\kilo": "\\text{k}",
|
|
||||||
"\\dezi": "\\text{d}",
|
|
||||||
"\\centi": "\\text{c}",
|
|
||||||
"\\milli": "\\text{m}",
|
|
||||||
"\\micro": "\\text{μ}",
|
|
||||||
"\\nano": "\\text{n}",
|
|
||||||
"\\pico": "\\text{p}",
|
|
||||||
"\\kilogram": "\\text{kg}",
|
|
||||||
"\\meter": "\\text{m}",
|
|
||||||
"\\second": "\\text{s}",
|
|
||||||
"\\ampere": "\\text{A}",
|
|
||||||
"\\kelvin": "\\text{K}",
|
|
||||||
"\\mol": "\\text{mol}",
|
|
||||||
"\\bel": "\\text{B}",
|
|
||||||
"\\dezibel": "\\text{dB}",
|
|
||||||
"\\dB": "\\text{dB}",
|
|
||||||
"\\dBm": "\\text{dBm}",
|
|
||||||
"\\dBu": "\\text{dBu}",
|
|
||||||
"\\dBW": "\\text{dBW}",
|
|
||||||
"\\dBi": "\\text{dBi}",
|
|
||||||
"\\dBd": "\\text{dBd}",
|
|
||||||
"\\candela": "\\text{cd}",
|
|
||||||
"\\newton": "\\text{N}",
|
|
||||||
"\\hertz": "\\text{Hz}",
|
|
||||||
"\\pascal": "\\text{Pa}",
|
|
||||||
"\\volt": "\\text{V}",
|
|
||||||
"\\watt": "\\text{W}",
|
|
||||||
"\\joule": "\\text{J}",
|
|
||||||
"\\henry": "\\text{H}",
|
|
||||||
"\\farad": "\\text{F}",
|
|
||||||
"\\coulomb": "\\text{C}",
|
|
||||||
"\\ohm": "\\Omega",
|
|
||||||
"\\weber": "\\text{Wb}",
|
|
||||||
"\\tesla": "\\text{T}",
|
|
||||||
};
|
|
||||||
|
|
||||||
renderMathInElement(document.body, {
|
|
||||||
delimiters: [
|
|
||||||
{left: '$$', right: '$$', display: true}, // Note: $$ has to come before $
|
|
||||||
{left: '$', right: '$', display: false},
|
|
||||||
{left: '\\[', right: '\\]', display: true},
|
|
||||||
{left: "\\(", right: "\\)", display: false},
|
|
||||||
],
|
|
||||||
throwOnError : false,
|
|
||||||
macros: macros
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function shuffleChildren(el) {
|
function shuffleChildren(el) {
|
||||||
for (var i = el.children.length; i >= 0; i--) {
|
for (var i = el.children.length; i >= 0; i--) {
|
||||||
el.appendChild(el.children[Math.random() * i | 0]);
|
el.appendChild(el.children[Math.random() * i | 0]);
|
||||||
@@ -71,6 +9,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
for (var i = 0; i < els.length; i++) {
|
for (var i = 0; i < els.length; i++) {
|
||||||
shuffleChildren(els[i]);
|
shuffleChildren(els[i]);
|
||||||
}
|
}
|
||||||
|
initQuizMode();
|
||||||
});
|
});
|
||||||
|
|
||||||
function answer(el, correct) {
|
function answer(el, correct) {
|
||||||
@@ -80,6 +19,76 @@ function answer(el, correct) {
|
|||||||
el.classList.add("wrong")
|
el.classList.add("wrong")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var NOTICE_ID = "quiz-mode-notice";
|
||||||
|
var STORAGE_KEY = "quiz-mode-enabled";
|
||||||
|
var QUIZ_ATTR = "data-quiz-mode";
|
||||||
|
var HAS_Q_ATTR = "data-quiz-has-questions";
|
||||||
|
|
||||||
|
function pageHasQuestions() {
|
||||||
|
return document.querySelector(".course .question") !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotice(text) {
|
||||||
|
var n = document.getElementById(NOTICE_ID);
|
||||||
|
if (!n) {
|
||||||
|
n = document.createElement("div");
|
||||||
|
n.id = NOTICE_ID;
|
||||||
|
n.className = "alert alert-info";
|
||||||
|
n.setAttribute("role", "alert");
|
||||||
|
n.style.clear = "both";
|
||||||
|
var course = document.querySelector(".course");
|
||||||
|
if (course) {
|
||||||
|
course.insertBefore(n, course.firstChild);
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideNotice() {
|
||||||
|
var n = document.getElementById(NOTICE_ID);
|
||||||
|
if (n) n.parentNode.removeChild(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButton(btn, on) {
|
||||||
|
btn.setAttribute("aria-pressed", on ? "true" : "false");
|
||||||
|
btn.classList.toggle("btn-primary", on);
|
||||||
|
btn.classList.toggle("btn-secondary", !on);
|
||||||
|
btn.title = on ? "Alles anzeigen" : "Nur Fragen anzeigen";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setQuizMode(on) {
|
||||||
|
var hasQ = pageHasQuestions();
|
||||||
|
if (hasQ) document.body.setAttribute(HAS_Q_ATTR, "");
|
||||||
|
else document.body.removeAttribute(HAS_Q_ATTR);
|
||||||
|
if (on) document.body.setAttribute(QUIZ_ATTR, "");
|
||||||
|
else document.body.removeAttribute(QUIZ_ATTR);
|
||||||
|
var btn = document.getElementById("quiz-mode-toggle");
|
||||||
|
if (btn) updateButton(btn, on);
|
||||||
|
if (on && !hasQ) {
|
||||||
|
showNotice("Keine Fragen auf dieser Seite gefunden.");
|
||||||
|
} else {
|
||||||
|
hideNotice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.initQuizMode = function () {
|
||||||
|
var btn = document.getElementById("quiz-mode-toggle");
|
||||||
|
if (!btn) return;
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
var next = !document.body.hasAttribute(QUIZ_ATTR);
|
||||||
|
setQuizMode(next);
|
||||||
|
try { localStorage.setItem(STORAGE_KEY, next ? "1" : "0"); } catch (e) {}
|
||||||
|
});
|
||||||
|
var initial = false;
|
||||||
|
try { initial = localStorage.getItem(STORAGE_KEY) === "1"; } catch (e) {}
|
||||||
|
if (initial) setQuizMode(true);
|
||||||
|
else updateButton(btn, false);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
var els = document.querySelectorAll(".question .question-text");
|
var els = document.querySelectorAll(".question .question-text");
|
||||||
|
|
||||||
@@ -87,6 +96,4 @@ for (i = 0; i < els.length; i++) {
|
|||||||
let el = els[i];
|
let el = els[i];
|
||||||
katex.render(el.innerHTML, el, { throwOnError: false });
|
katex.render(el.innerHTML, el, { throwOnError: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
+30
-7
@@ -19,11 +19,12 @@
|
|||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.question {
|
.question:not(.for-solution) {
|
||||||
padding: 1ex;
|
padding: 1ex;
|
||||||
background: #ddd;
|
background: #ddd;
|
||||||
margin-top: 2em;
|
margin-top: 2em;
|
||||||
margin-bottom: 2em;
|
margin-bottom: 2em;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.question p {
|
.question p {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -34,6 +35,20 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 1ex 1em 1.5ex 1em;
|
margin: 1ex 1em 1.5ex 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.solution {
|
||||||
|
margin-top: -1.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
text-align: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.solution .solution-icon {
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
display: block;
|
||||||
|
margin-right: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
.question .question-picture {
|
.question .question-picture {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
@@ -171,7 +186,7 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
list-style-type: square;
|
list-style-type: square;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul li::marker {
|
ul li::marker {
|
||||||
@@ -198,7 +213,6 @@ span.morse {
|
|||||||
font-size: 33%;
|
font-size: 33%;
|
||||||
position: relative;
|
position: relative;
|
||||||
bottom: 1.7ex;
|
bottom: 1.7ex;
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span.morse_char {
|
span.morse_char {
|
||||||
@@ -246,6 +260,7 @@ span.formula {
|
|||||||
.course ul,
|
.course ul,
|
||||||
.course ol,
|
.course ol,
|
||||||
.course .question,
|
.course .question,
|
||||||
|
.course .solution,
|
||||||
.course .table,
|
.course .table,
|
||||||
.course .katex-display {
|
.course .katex-display {
|
||||||
width: 59.3%;
|
width: 59.3%;
|
||||||
@@ -419,15 +434,23 @@ span.formula {
|
|||||||
|
|
||||||
/* Enable Line Breaks in Equations ... */
|
/* Enable Line Breaks in Equations ... */
|
||||||
.course .katex-display > .katex {
|
.course .katex-display > .katex {
|
||||||
white-space: normal
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add space between broken lines: */
|
/* Add space between broken lines: */
|
||||||
.course .katex-display .base {
|
.course .katex-display .base {
|
||||||
margin: 0.5em 0em
|
margin: 0.5em 0em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compensate by reducing space around display math */
|
/* Compensate by reducing space around display math */
|
||||||
.course .katex-display {
|
.course .katex-display {
|
||||||
margin: 1em 0em;
|
margin: 1em 0em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Quiz-Modus */
|
||||||
|
body[data-quiz-mode][data-quiz-has-questions] .course > * {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
body[data-quiz-mode][data-quiz-has-questions] .course > [data-quiz-shown] {
|
||||||
|
display: revert !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,31 +1,10 @@
|
|||||||
import api.directus as directus
|
|
||||||
import src.build as build
|
import src.build as build
|
||||||
import src.config as config
|
import src.config as config
|
||||||
import src.download as download
|
|
||||||
|
|
||||||
conf = config.Config()
|
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(conf)
|
|
||||||
|
|
||||||
# Download the text files:
|
|
||||||
dl.download_edition("N")
|
|
||||||
dl.download_edition("E")
|
|
||||||
dl.download_edition("A")
|
|
||||||
dl.download_edition("NE")
|
|
||||||
dl.download_edition("EA")
|
|
||||||
dl.download_edition("NEA")
|
|
||||||
|
|
||||||
# Download additional files:
|
|
||||||
# dl.download_question_metadata() # (now constant file metadata3b.json)
|
|
||||||
dl.download_photos()
|
|
||||||
dl.download_pictures()
|
|
||||||
dl.download_includes()
|
|
||||||
dl.download_snippets()
|
|
||||||
dl.download_content()
|
|
||||||
|
|
||||||
# Build Everything:
|
# Build Everything:
|
||||||
|
bd = build.Build(conf)
|
||||||
bd.build_website()
|
bd.build_website()
|
||||||
bd.build_edition("N")
|
bd.build_edition("N")
|
||||||
bd.build_edition("E")
|
bd.build_edition("E")
|
||||||
@@ -34,3 +13,7 @@ bd.build_edition("NE")
|
|||||||
bd.build_edition("EA")
|
bd.build_edition("EA")
|
||||||
bd.build_edition("NEA")
|
bd.build_edition("NEA")
|
||||||
bd.build_assets()
|
bd.build_assets()
|
||||||
|
bd.build_solutions()
|
||||||
|
bd.build_question_index()
|
||||||
|
bd.build_index()
|
||||||
|
bd.build_zip()
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
{
|
{
|
||||||
"directus_question_base_url" : "https://fragenkatalog.darc.de/",
|
"input" : "/home/your_path_to/50ohm-contents-dl"
|
||||||
"directus_question_api_key" : "",
|
|
||||||
"directus_content_base_url" : "https://redaktion.50ohm.de/",
|
|
||||||
"directus_content_api_key" : "",
|
|
||||||
"no_latex": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
Prüfungsfragen zum Erwerb von Amateurfunkprüfungsbescheinigungen
|
|
||||||
3. Auflage, März 2024
|
|
||||||
|
|
||||||
Hinweis:
|
|
||||||
========
|
|
||||||
|
|
||||||
Die Prüfungsfragen werden in maschinenlesbarer Form von der
|
|
||||||
Bundesnetzagentur für Elektrizität, Gas, Telekommunikation, Post und Eisenbahnen,
|
|
||||||
Tulpenfeld 4, 53113 Bonn
|
|
||||||
unter der Datenlizenz DL-DE->BY-2.0 bereitgestellt.
|
|
||||||
|
|
||||||
Die bereitgestellten Daten und Metadaten dürfen für die kommerzielle und nicht kommerzielle Nutzung insbesondere
|
|
||||||
vervielfältigt, ausgedruckt, präsentiert, verändert, bearbeitet sowie an Dritte übermittelt werden;
|
|
||||||
mit eigenen Daten und Daten Anderer zusammengeführt und zu selbständigen neuen Datensätzen verbunden werden;
|
|
||||||
in interne und externe Geschäftsprozesse, Produkte und Anwendungen in öffentlichen und nicht öffentlichen
|
|
||||||
elektronischen Netzwerken eingebunden werden.
|
|
||||||
|
|
||||||
Bei der Nutzung ist sicherzustellen, dass folgende Angaben als Quellenvermerk enthalten sind:
|
|
||||||
|
|
||||||
"Prüfungsfragen zum Erwerb von Amateurfunkprüfungsbescheinigungen, Bundesnetzagentur, 3. Auflage, März 2024,
|
|
||||||
(www.bundesnetzagentur.de/amateurfunk), Datenlizenz Deutschland – Namensnennung – Version 2.0
|
|
||||||
(www.govdata.de/dl-de/by-2-0)"
|
|
||||||
|
|
||||||
Veränderungen, Bearbeitungen, neue Gestaltungen oder sonstige Abwandlungen sind im Quellenvermerk mit dem Hinweis
|
|
||||||
zu versehen, dass die Daten geändert wurden.
|
|
||||||
|
|
||||||
|
|
||||||
Anleitung:
|
|
||||||
==========
|
|
||||||
|
|
||||||
Die maschinenlesbare Version der Prüfungsfragen teilt sich in zwei Teile auf:
|
|
||||||
|
|
||||||
1. Die Struktur des Katalogs und die Prüfungsfragen als JSON-Datei
|
|
||||||
2. Alle Bilder zu den Prüfungsfragen als SVG-Dateien
|
|
||||||
|
|
||||||
Das JSON-Schema besteht aus Kapiteln "sections", die jeweils einen Titel
|
|
||||||
"title" beinhalten. Eine Section kann entweder weitere Unterkapitel "sections"
|
|
||||||
beinhalten, oder eine Liste von Prüfungsfragen "questions". Eine Prüfungsfrage
|
|
||||||
beinhaltet folgende Attribute:
|
|
||||||
|
|
||||||
- "number" : Die Prüfungsnummer im Fragenkatalog
|
|
||||||
- "class" : Die Klasse (1:N, 2:E, 3:A)
|
|
||||||
- "question" : Der Fragenkopf
|
|
||||||
- "answer_a" : Die richtige Antwort A
|
|
||||||
- "answer_b" : Ein Distraktor B (falsche Antwort)
|
|
||||||
- "answer_c" : Ein Distraktor C (falsche Antwort)
|
|
||||||
- "answer_d" : Ein Distraktor D (falsche Antwort)
|
|
||||||
- "picture_question" : Das Fragenbild [Optional]
|
|
||||||
- "picture_a" : Das Antwortbild A [Optional]
|
|
||||||
- "picture_b" : Das Antwortbild B [Optional]
|
|
||||||
- "picture_c" : Das Antwortbild C [Optional]
|
|
||||||
- "picture_d" : Das Antwortbild D [Optional]
|
|
||||||
|
|
||||||
Die Prüfungsfragen enthalten LaTeX-Code für die Darstellung der Formeln. Diese
|
|
||||||
können durch verschiedene Bibliotheken z. B. durch KaTeX (https://katex.org)
|
|
||||||
angezeigt werden.
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
-15752
File diff suppressed because it is too large
Load Diff
+6
-19
@@ -1,22 +1,9 @@
|
|||||||
FROM python:3.12
|
FROM python:3.12-slim
|
||||||
|
|
||||||
# RUN apt-get update -qq && apt-get install -y \
|
WORKDIR /app
|
||||||
# wget \
|
COPY requirements.txt requirements.txt
|
||||||
# libswitch-perl \
|
RUN pip3 install -r requirements.txt
|
||||||
# gnupg \
|
|
||||||
# && apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
||||||
#
|
|
||||||
# COPY ./texlive.profile /tmp/
|
|
||||||
# RUN cd /tmp && wget -qO- https://mirror.ctan.org/systems/texlive/tlnet/install-tl-unx.tar.gz | tar xz \
|
|
||||||
# && /tmp/install-tl*/install-tl -profile /tmp/texlive.profile \
|
|
||||||
# && rm -rf /tmp/install-tl-*
|
|
||||||
#
|
|
||||||
# ENV PATH="/usr/local/texlive/bin/x86_64-linux:${PATH}"
|
|
||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
COPY --parents api assets latex renderer src templates main.py LICENSE ./
|
||||||
|
|
||||||
COPY --from=texlive/texlive:latest /usr/local/texlive /usr/local/texlive
|
ENTRYPOINT ["python3", "main.py"]
|
||||||
|
|
||||||
RUN $(find /usr/local/texlive -name tlmgr) path add && \
|
|
||||||
# Temporary fix for ConTeXt (#30)
|
|
||||||
(sed -i '/package.loaded\["data-ini"\]/a if os.selfpath then environment.ownbin=lfs.symlinktarget(os.selfpath..io.fileseparator..os.selfname);environment.ownpath=environment.ownbin:match("^.*"..io.fileseparator) else environment.ownpath=kpse.new("luatex"):var_value("SELFAUTOLOC");environment.ownbin=environment.ownpath..io.fileseparator..(arg[-2] or arg[-1] or arg[0] or "luatex"):match("[^"..io.fileseparator.."]*$") end' /usr/bin/mtxrun.lua || true)
|
|
||||||
@@ -1063,3 +1063,172 @@
|
|||||||
\circuitikzbasekey/bipole/is voltageoutsideofsymbol=true}
|
\circuitikzbasekey/bipole/is voltageoutsideofsymbol=true}
|
||||||
|
|
||||||
\makeatother
|
\makeatother
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
% Channel Coder
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/channelcoder/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/channelcoder/width}}
|
||||||
|
{channelcoder}
|
||||||
|
{\ctikzvalof{bipoles/channelcoder/width}}
|
||||||
|
{\ctikzvalof{bipoles/channelcoder/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 01}
|
||||||
|
\pgftext[center,x=0.30\pgf@circ@res@right,y=0.6\pgf@circ@res@down]{\tiny 01+01}
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{channelcoder}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
% Channel Decoder
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/channeldecoder/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/channeldecoder/width}}
|
||||||
|
{channeldecoder}
|
||||||
|
{\ctikzvalof{bipoles/channeldecoder/width}}
|
||||||
|
{\ctikzvalof{bipoles/channeldecoder/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
\pgftext[center,x=0.25\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 01+01}
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@right,y=0.6\pgf@circ@res@down]{\tiny 01}
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{channeldecoder}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
|
||||||
|
% Source Coder
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/sourcecoder/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/sourcecoder/width}}
|
||||||
|
{sourcecoder}
|
||||||
|
{\ctikzvalof{bipoles/sourcecoder/width}}
|
||||||
|
{\ctikzvalof{bipoles/sourcecoder/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
\pgftext[center,x=0.30\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 0101}
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@right,y=0.6\pgf@circ@res@down]{\tiny 01}
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{sourcecoder}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
% Source Decoder
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/sourcedecoder/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/sourcedecoder/width}}
|
||||||
|
{sourcedecoder}
|
||||||
|
{\ctikzvalof{bipoles/sourcedecoder/width}}
|
||||||
|
{\ctikzvalof{bipoles/sourcedecoder/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 01}
|
||||||
|
\pgftext[center,x=0.30\pgf@circ@res@right,y=0.6\pgf@circ@res@down]{\tiny 0101}
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{sourcedecoder}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
% Mapper
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/mapper/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/mapper/width}}
|
||||||
|
{mapper}
|
||||||
|
{\ctikzvalof{bipoles/mapper/width}}
|
||||||
|
{\ctikzvalof{bipoles/mapper/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@left,y=0.6\pgf@circ@res@up]{\tiny 01}
|
||||||
|
%
|
||||||
|
\pgfpathmoveto{\pgfpoint{0.50\pgf@circ@res@right}{-0.25\pgf@circ@res@up}}
|
||||||
|
\pgfpathlineto{\pgfpoint{0.50\pgf@circ@res@right}{-0.75\pgf@circ@res@up}}
|
||||||
|
\pgfusepath{draw}
|
||||||
|
%
|
||||||
|
\pgfpathmoveto{\pgfpoint{0.25\pgf@circ@res@right}{-0.50\pgf@circ@res@up}}
|
||||||
|
\pgfpathlineto{\pgfpoint{0.75\pgf@circ@res@right}{-0.50\pgf@circ@res@up}}
|
||||||
|
\pgfusepath{draw}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.65\pgf@circ@res@right}{-0.65\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.35\pgf@circ@res@right}{-0.65\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.65\pgf@circ@res@right}{-0.35\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.35\pgf@circ@res@right}{-0.35\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{mapper}
|
||||||
|
\makeatother
|
||||||
|
|
||||||
|
% Demapper
|
||||||
|
\makeatletter
|
||||||
|
\ctikzset{bipoles/demapper/width/.initial=.7}
|
||||||
|
|
||||||
|
\pgfcirc@define@twoports{blocks}
|
||||||
|
{}
|
||||||
|
{\ctikzvalof{bipoles/demapper/width}}
|
||||||
|
{demapper}
|
||||||
|
{\ctikzvalof{bipoles/demapper/width}}
|
||||||
|
{\ctikzvalof{bipoles/demapper/width}}
|
||||||
|
{
|
||||||
|
\pgfcirc@twoport@draw@splitline
|
||||||
|
%
|
||||||
|
\pgf@circ@text@strokecolor
|
||||||
|
%
|
||||||
|
\pgfpathmoveto{\pgfpoint{0.50\pgf@circ@res@left}{+0.25\pgf@circ@res@up}}
|
||||||
|
\pgfpathlineto{\pgfpoint{0.50\pgf@circ@res@left}{+0.75\pgf@circ@res@up}}
|
||||||
|
\pgfusepath{draw}
|
||||||
|
%
|
||||||
|
\pgfpathmoveto{\pgfpoint{0.25\pgf@circ@res@left}{+0.50\pgf@circ@res@up}}
|
||||||
|
\pgfpathlineto{\pgfpoint{0.75\pgf@circ@res@left}{+0.50\pgf@circ@res@up}}
|
||||||
|
\pgfusepath{draw}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.65\pgf@circ@res@left}{+0.65\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.35\pgf@circ@res@left}{+0.65\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.65\pgf@circ@res@left}{+0.35\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgfpathcircle{\pgfpoint{0.35\pgf@circ@res@left}{+0.35\pgf@circ@res@up}}{0.05\pgf@circ@res@up}
|
||||||
|
\pgfusepath{fill}
|
||||||
|
%
|
||||||
|
\pgftext[center,x=0.55\pgf@circ@res@right,y=-0.6\pgf@circ@res@up]{\tiny 01}
|
||||||
|
}
|
||||||
|
|
||||||
|
\pgfcirc@activate@bipole@simple{l}{demapper}
|
||||||
|
\makeatother
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
import src.build as build
|
||||||
|
import src.config as config
|
||||||
|
from src.edition import Edition
|
||||||
|
|
||||||
|
app = typer.Typer()
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def main(
|
||||||
|
edition: Annotated[list[Edition], typer.Option(help="Edition to build, can be specified multiple times.")] = [ # noqa: B006 -- default value is required for typer
|
||||||
|
Edition.n,
|
||||||
|
Edition.e,
|
||||||
|
Edition.a,
|
||||||
|
Edition.ne,
|
||||||
|
Edition.ea,
|
||||||
|
Edition.nea,
|
||||||
|
],
|
||||||
|
input: Annotated[str | None, typer.Option("--input", "-i", help="Content source directory.")] = None,
|
||||||
|
output: Annotated[str | None, typer.Option("--output", "-o", help="Destination directory to build to.")] = None,
|
||||||
|
render_editions: Annotated[bool, typer.Option(help="Skip building editions.")] = True,
|
||||||
|
render_solutions: Annotated[bool, typer.Option(help="Skip building solutions.")] = True,
|
||||||
|
build_zip: Annotated[bool, typer.Option(help="Whether to build a zip file of the output.")] = False,
|
||||||
|
) -> None:
|
||||||
|
conf = config.Config(content_path=input, build_path=output)
|
||||||
|
bd = build.Build(conf)
|
||||||
|
|
||||||
|
# Build surrounding website
|
||||||
|
bd.build_website()
|
||||||
|
|
||||||
|
if render_editions:
|
||||||
|
# Build individual editions
|
||||||
|
for e in edition:
|
||||||
|
bd.build_edition(e)
|
||||||
|
|
||||||
|
if render_solutions:
|
||||||
|
# Build solution pages
|
||||||
|
bd.build_solutions()
|
||||||
|
|
||||||
|
# Copy assets to build folder
|
||||||
|
bd.build_assets()
|
||||||
|
bd.build_question_index()
|
||||||
|
bd.build_index()
|
||||||
|
|
||||||
|
if build_zip:
|
||||||
|
# Build zip file of output
|
||||||
|
bd.build_zip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
+3
-2
@@ -6,10 +6,11 @@ readme = "README.md"
|
|||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jinja2==3.1.6",
|
"jinja2==3.1.6",
|
||||||
"joblib==1.5.1",
|
|
||||||
"mistletoe==1.4.0",
|
"mistletoe==1.4.0",
|
||||||
"requests==2.32.4",
|
"requests==2.32.4",
|
||||||
|
"rich>=14.2.0",
|
||||||
"tqdm==4.67.1",
|
"tqdm==4.67.1",
|
||||||
|
"typer>=0.21.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
@@ -57,4 +58,4 @@ addopts = ["--color=yes"]
|
|||||||
markers = ["html", "latex", "slide"]
|
markers = ["html", "latex", "slide"]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = ["pytest==8.4.1", "pytest-cov==6.2.1", "ruff==0.12.2", "ty==0.0.1a13"]
|
dev = ["pytest==8.4.1", "pytest-cov==6.2.1", "ruff==0.15.12", "ty==0.0.1a13"]
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from mistletoe import cli
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
cli.main(sys.argv[1:])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from mistletoe import HtmlRenderer
|
from mistletoe import HtmlRenderer
|
||||||
|
|
||||||
@@ -7,6 +9,7 @@ from .document import Document
|
|||||||
from .formula import Formula
|
from .formula import Formula
|
||||||
from .halfwidth_spaces import HalfwidthSpaces
|
from .halfwidth_spaces import HalfwidthSpaces
|
||||||
from .include import Include
|
from .include import Include
|
||||||
|
from .index import Index
|
||||||
from .morse import Morse
|
from .morse import Morse
|
||||||
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
||||||
from .photo import Photo
|
from .photo import Photo
|
||||||
@@ -71,6 +74,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
|||||||
Qso,
|
Qso,
|
||||||
Include,
|
Include,
|
||||||
Formula,
|
Formula,
|
||||||
|
Index,
|
||||||
*extras,
|
*extras,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
@@ -95,6 +99,9 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
|||||||
# Single unified counter for all figure types (pictures, photos, tables)
|
# Single unified counter for all figure types (pictures, photos, tables)
|
||||||
self.figure_counter = 0
|
self.figure_counter = 0
|
||||||
|
|
||||||
|
# Keep track of index ids already emitted in this document.
|
||||||
|
self.index_anchor_ids = set()
|
||||||
|
|
||||||
def get_reference_label(self, marker: str):
|
def get_reference_label(self, marker: str):
|
||||||
if marker in self.references:
|
if marker in self.references:
|
||||||
return f"{self.edition}-{self.chapter}.{self.section}.{self.references[marker]}", True
|
return f"{self.edition}-{self.chapter}.{self.section}.{self.references[marker]}", True
|
||||||
@@ -290,7 +297,7 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
|||||||
def render_photo_helper(id, ref, text, number, alt_text):
|
def render_photo_helper(id, ref, text, number, alt_text):
|
||||||
return f"""
|
return f"""
|
||||||
<figure class="photo" id="ref_{ref}" name="{ref}">
|
<figure class="photo" id="ref_{ref}" name="{ref}">
|
||||||
<img src="photos/{id}.jpg" alt="{alt_text}">
|
<img src="photos/{id}.png" alt="{alt_text}">
|
||||||
<figcaption>Abbildung {number}: {text}</figcaption>
|
<figcaption>Abbildung {number}: {text}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
"""
|
"""
|
||||||
@@ -345,3 +352,25 @@ class FiftyOhmHtmlRenderer(HtmlRenderer):
|
|||||||
|
|
||||||
def render_formula(self, token):
|
def render_formula(self, token):
|
||||||
return f"\n$${token.formula}$$\n"
|
return f"\n$${token.formula}$$\n"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_index_part(value: str) -> str:
|
||||||
|
normalized = value.strip().lower()
|
||||||
|
normalized = re.sub(r"\s+", "_", normalized)
|
||||||
|
normalized = re.sub(r"[^\w]", "_", normalized)
|
||||||
|
normalized = re.sub(r"_+", "_", normalized).strip("_")
|
||||||
|
return normalized or "index"
|
||||||
|
|
||||||
|
def render_index(self, token):
|
||||||
|
first = self._normalize_index_part(token.first)
|
||||||
|
if token.second:
|
||||||
|
second = self._normalize_index_part(token.second)
|
||||||
|
span_id = f"index_{first}__{second}"
|
||||||
|
else:
|
||||||
|
span_id = f"index_{first}"
|
||||||
|
|
||||||
|
if span_id in self.index_anchor_ids:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
self.index_anchor_ids.add(span_id)
|
||||||
|
return f'<span id="{span_id}" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from mistletoe.latex_renderer import LaTeXRenderer
|
|||||||
from .comment import BlockComment
|
from .comment import BlockComment
|
||||||
from .dash import Dash
|
from .dash import Dash
|
||||||
from .halfwidth_spaces import HalfwidthSpaces
|
from .halfwidth_spaces import HalfwidthSpaces
|
||||||
|
from .index import Index
|
||||||
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
from .nonbreaking_spaces import NonbreakingSpaces, NonbreakingSpacesDots
|
||||||
from .photo import Photo
|
from .photo import Photo
|
||||||
from .picture import Picture
|
from .picture import Picture
|
||||||
@@ -32,6 +33,7 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer):
|
|||||||
TableRow,
|
TableRow,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableCell,
|
TableCell,
|
||||||
|
Index,
|
||||||
)
|
)
|
||||||
self.question_renderer = question_renderer
|
self.question_renderer = question_renderer
|
||||||
|
|
||||||
@@ -148,3 +150,9 @@ class FiftyOhmLaTeXRenderer(LaTeXRenderer):
|
|||||||
|
|
||||||
def render_table_cell(self, token: TableCell):
|
def render_table_cell(self, token: TableCell):
|
||||||
return self.render_inner(token)
|
return self.render_inner(token)
|
||||||
|
|
||||||
|
def render_index(self, token):
|
||||||
|
if token.second:
|
||||||
|
return f"\\index{{{token.first}!{token.second}}}"
|
||||||
|
else:
|
||||||
|
return f"\\index{{{token.first}}}"
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from mistletoe.span_token import SpanToken
|
||||||
|
|
||||||
|
|
||||||
|
class Index(SpanToken):
|
||||||
|
"""
|
||||||
|
Resolve index commands
|
||||||
|
Identifies "[index:term:subterm]" or "[index:term]"
|
||||||
|
"""
|
||||||
|
|
||||||
|
pattern = re.compile(r"[ \t]*\[index:([^:\]\s](?:[^:\]\s]| )*)(?::([^:\]\s](?:[^:\]\s]| )*))?\]")
|
||||||
|
|
||||||
|
def __init__(self, match_object):
|
||||||
|
self.first = match_object.group(1)
|
||||||
|
self.second = match_object.group(2)
|
||||||
@@ -8,12 +8,12 @@ class Question(BlockToken):
|
|||||||
def start(line):
|
def start(line):
|
||||||
# Search for:
|
# Search for:
|
||||||
# [question:123]
|
# [question:123]
|
||||||
return re.match(r"^\s*\[question:\d+\]", line)
|
return re.match(r"^\s*\[question:[\w\d]+\]", line)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def read(cls, lines):
|
def read(cls, lines):
|
||||||
first_line = next(lines)
|
first_line = next(lines)
|
||||||
question_number = re.match(r"^\s*\[question:(\d+)\]", first_line).group(1)
|
question_number = re.match(r"^\s*\[question:([\w\d]+)\]", first_line).group(1)
|
||||||
return question_number
|
return question_number
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
+28
-9
@@ -1,19 +1,38 @@
|
|||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile pyproject.toml -o requirements.txt
|
||||||
certifi==2025.6.15
|
certifi==2025.6.15
|
||||||
|
# via requests
|
||||||
charset-normalizer==3.4.2
|
charset-normalizer==3.4.2
|
||||||
coverage==7.9.2
|
# via requests
|
||||||
|
click==8.3.1
|
||||||
|
# via typer
|
||||||
idna==3.10
|
idna==3.10
|
||||||
iniconfig==2.1.0
|
# via requests
|
||||||
jinja2==3.1.6
|
jinja2==3.1.6
|
||||||
joblib==1.5.1
|
# via 50ohm-de (pyproject.toml)
|
||||||
|
markdown-it-py==4.0.0
|
||||||
|
# via rich
|
||||||
markupsafe==3.0.2
|
markupsafe==3.0.2
|
||||||
|
# via jinja2
|
||||||
|
mdurl==0.1.2
|
||||||
|
# via markdown-it-py
|
||||||
mistletoe==1.4.0
|
mistletoe==1.4.0
|
||||||
packaging==25.0
|
# via 50ohm-de (pyproject.toml)
|
||||||
pluggy==1.6.0
|
|
||||||
pygments==2.19.2
|
pygments==2.19.2
|
||||||
pytest==8.4.1
|
# via rich
|
||||||
pytest-cov==6.2.1
|
|
||||||
requests==2.32.4
|
requests==2.32.4
|
||||||
ruff==0.12.2
|
# via 50ohm-de (pyproject.toml)
|
||||||
|
rich==14.3.2
|
||||||
|
# via
|
||||||
|
# 50ohm-de (pyproject.toml)
|
||||||
|
# typer
|
||||||
|
shellingham==1.5.4
|
||||||
|
# via typer
|
||||||
tqdm==4.67.1
|
tqdm==4.67.1
|
||||||
ty==0.0.1a13
|
# via 50ohm-de (pyproject.toml)
|
||||||
|
typer==0.21.1
|
||||||
|
# via 50ohm-de (pyproject.toml)
|
||||||
|
typing-extensions==4.15.0
|
||||||
|
# via typer
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
|
# via requests
|
||||||
|
|||||||
+300
-59
@@ -1,14 +1,19 @@
|
|||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from joblib import Memory
|
|
||||||
from mistletoe import Document
|
from mistletoe import Document
|
||||||
|
from rich.progress import BarColumn, Progress, TaskProgressColumn, TextColumn, TimeRemainingColumn
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||||
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
||||||
|
from renderer.index import Index
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
@@ -17,20 +22,18 @@ class Build:
|
|||||||
def __init__(self, config: Config):
|
def __init__(self, config: Config):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
memory = Memory("./cache", verbose=0)
|
self.env = Environment(loader=FileSystemLoader(self.config.p_templates))
|
||||||
self.env = Environment(loader=FileSystemLoader("templates/"))
|
|
||||||
self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers
|
self.env.filters["shuffle_answers"] = self.__filter_shuffle_answers
|
||||||
self.questions = self.__parse_katalog()
|
self.questions = self.__parse_katalog()
|
||||||
|
|
||||||
# Decorate the method with memory.cache
|
self.question_index = {}
|
||||||
self.__build_question = memory.cache(self.__build_question)
|
self.question_token_pattern = re.compile(r"^\s*\[question:([\w\d]+)\]", re.MULTILINE)
|
||||||
self.__build_question_slide = memory.cache(self.__build_question_slide)
|
|
||||||
self.__build_chapter = memory.cache(self.__build_chapter)
|
self.index_entries = {}
|
||||||
self.__build_section = memory.cache(self.__build_section)
|
self.index_token_pattern = Index.pattern
|
||||||
self.__build_chapter_slidedeck = memory.cache(self.__build_chapter_slidedeck)
|
|
||||||
|
|
||||||
def __parse_katalog(self):
|
def __parse_katalog(self):
|
||||||
with self.config.p_fragenkatalog.open() as file:
|
with self.config.p_data_fragenkatalog.open() as file:
|
||||||
fragenkatalog = json.load(file)
|
fragenkatalog = json.load(file)
|
||||||
|
|
||||||
questions = {}
|
questions = {}
|
||||||
@@ -47,27 +50,28 @@ class Build:
|
|||||||
|
|
||||||
return questions
|
return questions
|
||||||
|
|
||||||
# cached
|
def __build_question(self, number, template_file="html/question.html"):
|
||||||
def __build_question(self, input, template_file="html/question.html"):
|
|
||||||
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
||||||
|
|
||||||
question_template = self.env.get_template(template_file)
|
question_template = self.env.get_template(template_file)
|
||||||
|
|
||||||
with (self.config.p_data / "metadata3b.json").open() as file:
|
with (self.config.p_data_metadata).open() as file:
|
||||||
metadata = json.load(file)
|
metadata_json = json.load(file)
|
||||||
|
|
||||||
question = None
|
question = None
|
||||||
number = None
|
metadata = None
|
||||||
if f"{input}" in metadata:
|
|
||||||
metadata = metadata[f"{input}"]
|
|
||||||
number = metadata["number"] # Fragennummer z.B. AB123
|
|
||||||
if number in self.questions:
|
|
||||||
question = self.questions[number]
|
|
||||||
|
|
||||||
if question is None:
|
if number in self.questions:
|
||||||
|
question = self.questions[number]
|
||||||
|
|
||||||
|
if number in metadata_json:
|
||||||
|
metadata = metadata_json[number]
|
||||||
|
|
||||||
|
if question is None or metadata is None:
|
||||||
tqdm.write(
|
tqdm.write(
|
||||||
f"\033[31mQuestion #{input} is missing"
|
f"\033[31mQuestion #{number} is missing"
|
||||||
+ (f" (but found number: {number})" if number is not None else "")
|
+ (" (Question not in question pool)" if question is None else "")
|
||||||
|
+ (" (Question not in metadata)" if metadata is None else "")
|
||||||
+ "\033[0m"
|
+ "\033[0m"
|
||||||
)
|
)
|
||||||
metadata = {"layout": "not-found", "picture_a": ""}
|
metadata = {"layout": "not-found", "picture_a": ""}
|
||||||
@@ -110,6 +114,8 @@ class Build:
|
|||||||
picture_question = ""
|
picture_question = ""
|
||||||
alt_text_question = ""
|
alt_text_question = ""
|
||||||
|
|
||||||
|
solution_file = self.config.p_data_solutions / f"{number}.md"
|
||||||
|
|
||||||
return question_template.render(
|
return question_template.render(
|
||||||
question=question["question"],
|
question=question["question"],
|
||||||
number=number,
|
number=number,
|
||||||
@@ -119,18 +125,17 @@ class Build:
|
|||||||
answer_pictures=answer_pictures,
|
answer_pictures=answer_pictures,
|
||||||
alt_text_answers=alt_text_answers,
|
alt_text_answers=alt_text_answers,
|
||||||
alt_text_question=alt_text_question,
|
alt_text_question=alt_text_question,
|
||||||
|
has_solution=solution_file.exists(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __build_question_slide(self, input):
|
def __build_question_slide(self, input):
|
||||||
return self.__build_question(input, template_file="slide/question.html")
|
return self.__build_question(input, template_file="slide/question.html")
|
||||||
|
|
||||||
# cached
|
|
||||||
def __build_page(self, content, course_wrapper=False, sidebar=None):
|
def __build_page(self, content, course_wrapper=False, sidebar=None):
|
||||||
page_template = self.env.get_template("html/page.html")
|
page_template = self.env.get_template("html/page.html")
|
||||||
return page_template.render(content=content, course_wrapper=course_wrapper, sidebar=sidebar)
|
return page_template.render(content=content, course_wrapper=course_wrapper, sidebar=sidebar)
|
||||||
|
|
||||||
def __picture_handler(self, id):
|
def __copy_picture(self, id):
|
||||||
self.config.p_build_pictures.mkdir(parents=True, exist_ok=True)
|
|
||||||
file = f"{id}.svg"
|
file = f"{id}.svg"
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(self.config.p_data_pictures / file, self.config.p_build_pictures / file)
|
shutil.copyfile(self.config.p_data_pictures / file, self.config.p_build_pictures / file)
|
||||||
@@ -141,9 +146,13 @@ class Build:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
tqdm.write(f"\033[31mPicture #{id} not found\033[0m")
|
tqdm.write(f"\033[31mPicture #{id} not found\033[0m")
|
||||||
|
|
||||||
|
def __picture_handler(self, id):
|
||||||
|
self.config.p_build_pictures.mkdir(parents=True, exist_ok=True)
|
||||||
|
return self.__copy_picture(id)
|
||||||
|
|
||||||
def __photo_handler(self, id):
|
def __photo_handler(self, id):
|
||||||
self.config.p_build_photos.mkdir(parents=True, exist_ok=True)
|
self.config.p_build_photos.mkdir(parents=True, exist_ok=True)
|
||||||
file = f"{id}.jpg"
|
file = f"{id}.png"
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(self.config.p_data_photos / file, self.config.p_build_photos / file)
|
shutil.copyfile(self.config.p_data_photos / file, self.config.p_build_photos / file)
|
||||||
if (self.config.p_data_photos / f"{id}.txt").exists():
|
if (self.config.p_data_photos / f"{id}.txt").exists():
|
||||||
@@ -153,8 +162,7 @@ class Build:
|
|||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
tqdm.write(f"\033[31mPhoto #{id} not found\033[0m")
|
tqdm.write(f"\033[31mPhoto #{id} not found\033[0m")
|
||||||
|
|
||||||
# cached
|
def __build_chapter_index(self, edition, edition_name, number, chapter, next_chapter=None):
|
||||||
def __build_chapter(self, edition, edition_name, number, chapter, next_chapter=None):
|
|
||||||
chapter_template = self.env.get_template("html/chapter.html")
|
chapter_template = self.env.get_template("html/chapter.html")
|
||||||
next_chapter_template = self.env.get_template("html/next_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:
|
with (self.config.p_build / f"{edition}_chapter_{chapter['ident']}.html").open("w") as file:
|
||||||
@@ -175,11 +183,13 @@ class Build:
|
|||||||
file.write(result)
|
file.write(result)
|
||||||
|
|
||||||
def __include_handler(self, include):
|
def __include_handler(self, include):
|
||||||
with (self.config.p_data / "includes.json").open() as file:
|
with (self.config.p_data_html / f"{include}.html").open() as file:
|
||||||
includes = json.load(file)
|
code = file.read()
|
||||||
return includes.get(include)
|
svg_list = re.findall(r"(\d+)\.svg", code or "")
|
||||||
|
for id in svg_list:
|
||||||
|
self.__copy_picture(id)
|
||||||
|
return code
|
||||||
|
|
||||||
# cached
|
|
||||||
def __build_section(
|
def __build_section(
|
||||||
self,
|
self,
|
||||||
edition,
|
edition,
|
||||||
@@ -241,7 +251,9 @@ class Build:
|
|||||||
result = self.__build_page(result, course_wrapper=True)
|
result = self.__build_page(result, course_wrapper=True)
|
||||||
file.write(result)
|
file.write(result)
|
||||||
|
|
||||||
def __build_chapter_slidedeck(self, edition, chapter, sections, next_chapter, chapter_number=None):
|
def __build_chapter_slidedeck(
|
||||||
|
self, edition, chapter, sections, next_chapter, chapter_number=None, progress: Progress = None
|
||||||
|
):
|
||||||
with (self.config.p_build / f"{edition}_slide_{chapter['ident']}.html").open("w") as file:
|
with (self.config.p_build / f"{edition}_slide_{chapter['ident']}.html").open("w") as file:
|
||||||
slide_template = self.env.get_template("slide/slide.html")
|
slide_template = self.env.get_template("slide/slide.html")
|
||||||
help_template = self.env.get_template("slide/help.html")
|
help_template = self.env.get_template("slide/help.html")
|
||||||
@@ -270,7 +282,10 @@ class Build:
|
|||||||
result += "</section>\n"
|
result += "</section>\n"
|
||||||
|
|
||||||
section_counter = 0
|
section_counter = 0
|
||||||
for section in sections:
|
slides_task = progress.add_task("Rendering slides ...")
|
||||||
|
for section in progress.track(sections, task_id=slides_task):
|
||||||
|
progress.update(slides_task, description=f"Rendering slides of {section['title']}")
|
||||||
|
|
||||||
if section["slide"] is None:
|
if section["slide"] is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -293,6 +308,8 @@ class Build:
|
|||||||
tmp += renderer.render(doc)
|
tmp += renderer.render(doc)
|
||||||
result += f"<section>{tmp}</section>\n"
|
result += f"<section>{tmp}</section>\n"
|
||||||
|
|
||||||
|
progress.remove_task(slides_task)
|
||||||
|
|
||||||
result += next_template.render(
|
result += next_template.render(
|
||||||
edition=edition,
|
edition=edition,
|
||||||
next_chapter=next_chapter,
|
next_chapter=next_chapter,
|
||||||
@@ -332,24 +349,71 @@ class Build:
|
|||||||
result = self.__build_page(result)
|
result = self.__build_page(result)
|
||||||
file.write(result)
|
file.write(result)
|
||||||
|
|
||||||
def build_edition(self, edition):
|
def build_edition(self, edition: str):
|
||||||
self.config.p_build.mkdir(exist_ok=True)
|
self.config.p_build.mkdir(exist_ok=True)
|
||||||
|
|
||||||
edition = edition.upper()
|
edition = edition.upper()
|
||||||
|
|
||||||
with (self.config.p_data / f"book_{edition}.json").open() as file:
|
with (
|
||||||
|
(self.config.p_data_toc / f"{edition}.json").open() as file,
|
||||||
|
Progress(
|
||||||
|
TaskProgressColumn(),
|
||||||
|
BarColumn(),
|
||||||
|
TimeRemainingColumn(),
|
||||||
|
TextColumn("[progress.description]{task.description}"),
|
||||||
|
transient=True,
|
||||||
|
) as progress,
|
||||||
|
):
|
||||||
|
chapter_task = progress.add_task(f"Building edition {edition} ...")
|
||||||
book = json.load(file)
|
book = json.load(file)
|
||||||
chapters = book["chapters"]
|
|
||||||
edition_name = book["title"]
|
edition_name = book["title"]
|
||||||
|
|
||||||
|
# Create index pages for chapters and slides.
|
||||||
self.__build_book_index(book)
|
self.__build_book_index(book)
|
||||||
self.__build_slide_index(book)
|
self.__build_slide_index(book)
|
||||||
for chapter_number, chapter in enumerate(tqdm(chapters, desc=f"Build Edition: {edition}"), 1):
|
|
||||||
next_chapter = chapters[chapter_number] if chapter_number < len(chapters) else None
|
|
||||||
self.__build_chapter(edition, edition_name, chapter_number, chapter, next_chapter)
|
|
||||||
self.__build_chapter_slidedeck(edition, chapter, chapter["sections"], next_chapter, chapter_number)
|
|
||||||
|
|
||||||
for section_number, section in enumerate(chapter["sections"], 1):
|
chapters = book["chapters"]
|
||||||
tqdm.write(f"Rendering section {section['title']}")
|
|
||||||
|
for chapter_number, chapter in enumerate(progress.track(chapters, task_id=chapter_task), 1):
|
||||||
|
progress.update(chapter_task, description=f"Building edition {edition}: Chapter {chapter['title']}")
|
||||||
|
|
||||||
|
# Determine next chapter for navigation (None if this is the last chapter)
|
||||||
|
next_chapter = chapters[chapter_number] if chapter_number < len(chapters) else None
|
||||||
|
|
||||||
|
self.__build_chapter_index(edition, edition_name, chapter_number, chapter, next_chapter)
|
||||||
|
|
||||||
|
# Open, parse and render each section.
|
||||||
|
section_task = progress.add_task(description="Rendering sections ...")
|
||||||
|
for section_number, section in enumerate(progress.track(chapter["sections"], task_id=section_task), 1):
|
||||||
|
progress.update(section_task, description=f"Rendering section {section['title']}")
|
||||||
|
|
||||||
|
# Read section and slide content from the corresponding files.
|
||||||
|
ident = section["ident"]
|
||||||
|
section["content"] = None
|
||||||
|
section["slide"] = None
|
||||||
|
with (self.config.p_data_sections / f"{ident}.md").open() as sfile:
|
||||||
|
section_content = sfile.read()
|
||||||
|
section["content"] = section_content
|
||||||
|
with (self.config.p_data_slides / f"{ident}.md").open() as sfile:
|
||||||
|
section_content = sfile.read()
|
||||||
|
section["slide"] = section_content
|
||||||
|
|
||||||
|
if section["content"] is not None:
|
||||||
|
self.__collect_question_occurrences(
|
||||||
|
edition,
|
||||||
|
chapter["title"],
|
||||||
|
ident,
|
||||||
|
section["title"],
|
||||||
|
section["content"],
|
||||||
|
)
|
||||||
|
self.__collect_index_occurrences(
|
||||||
|
edition,
|
||||||
|
chapter["title"],
|
||||||
|
ident,
|
||||||
|
section["title"],
|
||||||
|
section["content"],
|
||||||
|
)
|
||||||
|
|
||||||
next_section = (
|
next_section = (
|
||||||
chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None
|
chapter["sections"][section_number] if section_number < len(chapter["sections"]) else None
|
||||||
)
|
)
|
||||||
@@ -364,6 +428,14 @@ class Build:
|
|||||||
chapter_number,
|
chapter_number,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
progress.remove_task(section_task)
|
||||||
|
# Build the slidedeck after all sections have been processed, as they require the files to be read.
|
||||||
|
self.__build_chapter_slidedeck(
|
||||||
|
edition, chapter, chapter["sections"], next_chapter, chapter_number, progress
|
||||||
|
)
|
||||||
|
|
||||||
|
progress.remove_task(chapter_task)
|
||||||
|
|
||||||
def build_assets(self):
|
def build_assets(self):
|
||||||
self.config.p_build.mkdir(exist_ok=True)
|
self.config.p_build.mkdir(exist_ok=True)
|
||||||
shutil.copytree(
|
shutil.copytree(
|
||||||
@@ -371,25 +443,47 @@ class Build:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __parse_snippets(self):
|
def __parse_snippets(self):
|
||||||
with (self.config.p_data / "snippets.json").open() as file:
|
snippets = {}
|
||||||
snippets = json.load(file)
|
|
||||||
|
for md_file in Path(self.config.p_data_snippets).glob("*.md"):
|
||||||
|
with md_file.open() as file:
|
||||||
|
snippets[md_file.stem] = file.read()
|
||||||
|
|
||||||
|
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>:
|
||||||
|
snippets[key] = snippets[key][3:-4]
|
||||||
|
|
||||||
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>:
|
|
||||||
snippets[key] = snippets[key][3:-4]
|
|
||||||
return snippets
|
return snippets
|
||||||
|
|
||||||
def __parse_contents(self):
|
def __parse_contents(self):
|
||||||
with (self.config.p_data / "content.json").open() as file:
|
static = []
|
||||||
contents = json.load(file)
|
|
||||||
return contents
|
for static_file in Path(self.config.p_data_static).glob("*.html"):
|
||||||
|
if "sidebar" not in static_file.stem:
|
||||||
|
with static_file.open() as file:
|
||||||
|
content = file.read()
|
||||||
|
sidebar_file = self.config.p_data_static / f"{static_file.stem}_sidebar.html"
|
||||||
|
if not sidebar_file.exists():
|
||||||
|
sidebar = ""
|
||||||
|
else:
|
||||||
|
with sidebar_file.open() as file:
|
||||||
|
sidebar = file.read()
|
||||||
|
|
||||||
|
static.append(
|
||||||
|
{
|
||||||
|
"url_part": static_file.stem,
|
||||||
|
"content": content,
|
||||||
|
"sidebar": sidebar if sidebar != "" else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return static
|
||||||
|
|
||||||
def __build_index(self, snippets):
|
def __build_index(self, snippets):
|
||||||
template = self.env.get_template("html/index.html")
|
template = self.env.get_template("html/index.html")
|
||||||
@@ -426,3 +520,150 @@ class Build:
|
|||||||
self.__build_course_page(snippets, "patenkarte", "patenkarte")
|
self.__build_course_page(snippets, "patenkarte", "patenkarte")
|
||||||
self.__build_html_page(contents, "pruefung")
|
self.__build_html_page(contents, "pruefung")
|
||||||
self.__build_html_page(contents, "infos")
|
self.__build_html_page(contents, "infos")
|
||||||
|
self.__build_html_page(contents, "suche")
|
||||||
|
|
||||||
|
def build_solutions(self):
|
||||||
|
for solution_file in self.config.p_data_solutions.glob("*.md"):
|
||||||
|
with solution_file.open() as file:
|
||||||
|
content = file.read()
|
||||||
|
with (self.config.p_build / f"{solution_file.stem}.html").open("w") as file:
|
||||||
|
with FiftyOhmHtmlRenderer(
|
||||||
|
question_renderer=self.__build_question,
|
||||||
|
picture_handler=self.__picture_handler,
|
||||||
|
photo_handler=self.__photo_handler,
|
||||||
|
include_handler=self.__include_handler,
|
||||||
|
) as renderer:
|
||||||
|
question = self.__build_question(
|
||||||
|
solution_file.stem, template_file="html/solution_question.html"
|
||||||
|
)
|
||||||
|
solution_template = self.env.get_template("html/solution.html")
|
||||||
|
solution = renderer.render(Document(content))
|
||||||
|
page = solution_template.render(question=question, solution=solution, number=solution_file.stem)
|
||||||
|
page = self.__build_page(page, course_wrapper=False)
|
||||||
|
file.write(page)
|
||||||
|
|
||||||
|
def __collect_question_occurrences(
|
||||||
|
self,
|
||||||
|
edition: str,
|
||||||
|
chapter_title: str,
|
||||||
|
section_ident: str,
|
||||||
|
section_title: str,
|
||||||
|
section_content: str,
|
||||||
|
):
|
||||||
|
for question_number in self.question_token_pattern.findall(section_content or ""):
|
||||||
|
question_entry = self.question_index.setdefault(
|
||||||
|
question_number,
|
||||||
|
{
|
||||||
|
"has_solution": self.__has_solution(question_number),
|
||||||
|
"section": section_ident,
|
||||||
|
"chapter_title": chapter_title,
|
||||||
|
"section_title": section_title,
|
||||||
|
"editions": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if edition not in question_entry["editions"]:
|
||||||
|
question_entry["editions"].append(edition)
|
||||||
|
|
||||||
|
def __has_solution(self, question_number: str) -> bool:
|
||||||
|
return (self.config.p_data_solutions / f"{question_number}.md").exists()
|
||||||
|
|
||||||
|
def __collect_index_occurrences(
|
||||||
|
self,
|
||||||
|
edition: str,
|
||||||
|
chapter_title: str,
|
||||||
|
section_ident: str,
|
||||||
|
section_title: str,
|
||||||
|
section_content: str,
|
||||||
|
):
|
||||||
|
for match in self.index_token_pattern.finditer(section_content or ""):
|
||||||
|
first = match.group(1).strip()
|
||||||
|
second = match.group(2).strip() if match.group(2) else None
|
||||||
|
|
||||||
|
normalized_first = FiftyOhmHtmlRenderer._normalize_index_part(first)
|
||||||
|
if second:
|
||||||
|
normalized_second = FiftyOhmHtmlRenderer._normalize_index_part(second)
|
||||||
|
anchor_id = f"index_{normalized_first}__{normalized_second}"
|
||||||
|
else:
|
||||||
|
anchor_id = f"index_{normalized_first}"
|
||||||
|
|
||||||
|
entry_key = f"{section_ident}#{anchor_id}"
|
||||||
|
index_entry = self.index_entries.setdefault(
|
||||||
|
entry_key,
|
||||||
|
{
|
||||||
|
"term": first,
|
||||||
|
"subterm": second,
|
||||||
|
"anchor_id": anchor_id,
|
||||||
|
"section": section_ident,
|
||||||
|
"chapter_title": chapter_title,
|
||||||
|
"section_title": section_title,
|
||||||
|
"editions": [],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if edition not in index_entry["editions"]:
|
||||||
|
index_entry["editions"].append(edition)
|
||||||
|
|
||||||
|
def build_question_index(self):
|
||||||
|
tqdm.write("Creating question index")
|
||||||
|
for question_data in self.question_index.values():
|
||||||
|
question_data["editions"] = sorted(question_data["editions"])
|
||||||
|
|
||||||
|
path = self.config.p_build_assets / "question_index.json"
|
||||||
|
with (path).open("w", encoding="utf-8") as file:
|
||||||
|
json.dump(self.question_index, file, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
|
file.write("\n")
|
||||||
|
|
||||||
|
def build_index(self):
|
||||||
|
tqdm.write("Creating index")
|
||||||
|
for index_data in self.index_entries.values():
|
||||||
|
index_data["editions"] = sorted(index_data["editions"])
|
||||||
|
|
||||||
|
entries = sorted(
|
||||||
|
self.index_entries.values(),
|
||||||
|
key=lambda item: (
|
||||||
|
item["term"].casefold(),
|
||||||
|
(item["subterm"] or "").casefold(),
|
||||||
|
item["section"],
|
||||||
|
item["anchor_id"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
path = self.config.p_build_assets / "index.json"
|
||||||
|
with (path).open("w", encoding="utf-8") as file:
|
||||||
|
json.dump(entries, file, ensure_ascii=False, indent=2)
|
||||||
|
file.write("\n")
|
||||||
|
|
||||||
|
def build_zip(self, zip_name: str | None = None) -> Path:
|
||||||
|
"""Create a zip archive of the complete build output directory.
|
||||||
|
|
||||||
|
The zip file is written into the build directory itself and is excluded
|
||||||
|
from the archive contents.
|
||||||
|
"""
|
||||||
|
|
||||||
|
build_dir = self.config.p_build
|
||||||
|
build_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
zip_path = build_dir / (zip_name if zip_name is not None else f"{build_dir.name}.zip")
|
||||||
|
zip_path = zip_path.resolve()
|
||||||
|
|
||||||
|
# If a previous archive exists, remove it first to avoid zipping stale data.
|
||||||
|
if zip_path.exists():
|
||||||
|
zip_path.unlink()
|
||||||
|
|
||||||
|
tqdm.write(f"Creating zip archive: {zip_path}")
|
||||||
|
|
||||||
|
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
# os.walk includes dotfiles; sort for stable archives.
|
||||||
|
for root, dirs, files in os.walk(build_dir):
|
||||||
|
dirs.sort()
|
||||||
|
files.sort()
|
||||||
|
|
||||||
|
root_path = Path(root)
|
||||||
|
for filename in files:
|
||||||
|
file_path = (root_path / filename).resolve()
|
||||||
|
if file_path == zip_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
arcname = file_path.relative_to(build_dir.resolve())
|
||||||
|
zf.write(file_path, arcname=arcname)
|
||||||
|
|
||||||
|
return zip_path
|
||||||
|
|||||||
+18
-11
@@ -6,31 +6,38 @@ from pathlib import Path
|
|||||||
class Config:
|
class Config:
|
||||||
__env_prefix = "OHM"
|
__env_prefix = "OHM"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, content_path: str = None, build_path: str = None):
|
||||||
if os.path.isfile("config/config.json"):
|
if os.path.isfile("config/config.json"):
|
||||||
with open("config/config.json") as file:
|
with open("config/config.json") as file:
|
||||||
self.config = json.load(file)
|
self.config = json.load(file)
|
||||||
else:
|
else:
|
||||||
self.config = {}
|
self.config = {}
|
||||||
|
|
||||||
self.question_access_token = self.get_config_value("directus_question_api_key")
|
self.p_data = Path(self.get_config_value("input", "content") if content_path is None else content_path)
|
||||||
self.question_base_url = self.get_config_value("directus_question_base_url", "https://fragenkatalog.darc.de/")
|
|
||||||
self.content_access_token = self.get_config_value("directus_content_api_key")
|
|
||||||
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_toc = self.p_data / "toc"
|
||||||
|
|
||||||
self.p_data = Path("./data")
|
self.p_data_contents = self.p_data / "contents"
|
||||||
self.p_data_photos = self.p_data / "photos"
|
self.p_data_questions = self.p_data_contents / "questions"
|
||||||
self.p_data_pictures = self.p_data / "pictures"
|
self.p_data_html = self.p_data_contents / "html"
|
||||||
|
self.p_data_photos = self.p_data_contents / "photos"
|
||||||
|
self.p_data_pictures = self.p_data_contents / "drawings"
|
||||||
|
self.p_data_sections = self.p_data_contents / "sections"
|
||||||
|
self.p_data_slides = self.p_data_contents / "slides"
|
||||||
|
self.p_data_snippets = self.p_data_contents / "snippets"
|
||||||
|
self.p_data_static = self.p_data_contents / "static"
|
||||||
|
self.p_data_solutions = self.p_data_contents / "solutions"
|
||||||
|
|
||||||
self.p_build = Path("./build")
|
self.p_data_fragenkatalog = self.p_data_questions / "fragenkatalog3b.json"
|
||||||
|
self.p_data_metadata = self.p_data_questions / "metadata3b.json"
|
||||||
|
|
||||||
|
self.p_build = Path(self.get_config_value("output", "./build") if build_path is None else build_path)
|
||||||
self.p_build_photos = self.p_build / "photos"
|
self.p_build_photos = self.p_build / "photos"
|
||||||
self.p_build_pictures = self.p_build / "pictures"
|
self.p_build_pictures = self.p_build / "pictures"
|
||||||
self.p_build_assets = self.p_build / "assets"
|
self.p_build_assets = self.p_build / "assets"
|
||||||
|
|
||||||
self.p_assets = Path("./assets")
|
self.p_assets = Path("./assets")
|
||||||
|
self.p_templates = Path("./templates")
|
||||||
|
|
||||||
def get_config_value(self, key: str, default=None):
|
def get_config_value(self, key: str, default=None):
|
||||||
if key in self.config:
|
if key in self.config:
|
||||||
|
|||||||
-162
@@ -1,162 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
from tqdm import tqdm
|
|
||||||
|
|
||||||
from api.directus import DirectusAPI
|
|
||||||
|
|
||||||
from .config import Config
|
|
||||||
|
|
||||||
|
|
||||||
class Download:
|
|
||||||
def __init__(self, api: DirectusAPI, content_api: DirectusAPI, config: Config):
|
|
||||||
self.api = api
|
|
||||||
self.config = config
|
|
||||||
self.content_api = content_api
|
|
||||||
|
|
||||||
def download_edition(self, edition):
|
|
||||||
in_edition = self.api.get_one("items/edition", {"filter": {"ident": {"_eq": edition}}})
|
|
||||||
|
|
||||||
in_chapters = self.api.get(
|
|
||||||
"items/chapter",
|
|
||||||
{"filter": {"edition": {"edition_id": {"_eq": in_edition["id"]}}}},
|
|
||||||
)
|
|
||||||
|
|
||||||
out_chapters = []
|
|
||||||
|
|
||||||
for in_chapter in tqdm(in_chapters, desc="Downloading edition"):
|
|
||||||
in_sections = self.api.get(
|
|
||||||
"items/section",
|
|
||||||
{"filter": {"chapter": {"chapter_id": {"_eq": in_chapter["id"]}}}},
|
|
||||||
)
|
|
||||||
|
|
||||||
out_chapters.append(
|
|
||||||
{
|
|
||||||
"title": in_chapter["title"],
|
|
||||||
"abstract": in_chapter["abstract"],
|
|
||||||
"ident": in_chapter["ident"],
|
|
||||||
"video_url": in_chapter["video_url"],
|
|
||||||
"sections": [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
for in_section in in_sections:
|
|
||||||
out_chapters[-1]["sections"].append(
|
|
||||||
{
|
|
||||||
"title": in_section["title"],
|
|
||||||
"ident": in_section["ident"],
|
|
||||||
"status": in_section["status"],
|
|
||||||
"class": in_section["class"],
|
|
||||||
"content": in_section["content"],
|
|
||||||
"slide": in_section["slide"],
|
|
||||||
"video_url": in_section["video_url"],
|
|
||||||
"questions": [],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
out_edition = {
|
|
||||||
"title": in_edition["title"],
|
|
||||||
"abstract": in_edition["abstract"],
|
|
||||||
"chapters": out_chapters,
|
|
||||||
"edition": edition,
|
|
||||||
}
|
|
||||||
|
|
||||||
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):
|
|
||||||
questions = self.api.get(
|
|
||||||
"items/questions", params={"filter": {"class_3": {"_in": [1, 2, 3]}}, "sort": "position", "limit": -1}
|
|
||||||
)
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
|
|
||||||
for question in tqdm(questions, desc="Downloading metadata"):
|
|
||||||
result[question["id"]] = {
|
|
||||||
"number": question["number"],
|
|
||||||
"picture_question": question["picture_question"] if question["picture_question"] is not None else "",
|
|
||||||
"picture_a": question["picture_a"] if question["picture_a"] is not None else "",
|
|
||||||
"picture_b": question["picture_b"] if question["picture_b"] is not None else "",
|
|
||||||
"picture_c": question["picture_c"] if question["picture_c"] is not None else "",
|
|
||||||
"picture_d": question["picture_d"] if question["picture_d"] is not None else "",
|
|
||||||
"layout": question["layout"] if question["layout"] is not None else "",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Serializing json:
|
|
||||||
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"):
|
|
||||||
# Save Photo to file
|
|
||||||
data = self.api.get_file("assets/" + photo["photo"])
|
|
||||||
with (self.config.p_data_photos / f"{photo['id']}.jpg").open("wb") as file:
|
|
||||||
file.write(data)
|
|
||||||
|
|
||||||
# Save Alt-Text to file
|
|
||||||
prefix = ""
|
|
||||||
if photo["alt_text"] is not None:
|
|
||||||
if photo["alt_text_reviewed"] is None:
|
|
||||||
prefix = "Der folgende Alt-Text wurde noch nicht geprüft: "
|
|
||||||
with (self.config.p_data_photos / f"{photo['id']}.txt").open("w") as file:
|
|
||||||
file.write(prefix + photo["alt_text"])
|
|
||||||
|
|
||||||
def download_pictures(self):
|
|
||||||
self.config.p_data_pictures.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
pictures = self.api.get("items/pictures", {"limit": -1})
|
|
||||||
|
|
||||||
for picture in tqdm(pictures, desc="Downloading pictures"):
|
|
||||||
# Save LaTeX sources to file
|
|
||||||
with (self.config.p_data_pictures / f"{picture['id']}include.tex").open("w") as file:
|
|
||||||
file.write(picture["latex"])
|
|
||||||
|
|
||||||
# Save Alt-Text to file
|
|
||||||
prefix = ""
|
|
||||||
if picture["alt_text"] is not None:
|
|
||||||
if picture["alt_text_reviewed"] is None:
|
|
||||||
prefix = "Der folgende Alt-Text wurde noch nicht geprüft: "
|
|
||||||
with (self.config.p_data_pictures / f"{picture['id']}.txt").open("w") as file:
|
|
||||||
file.write(prefix + picture["alt_text"])
|
|
||||||
|
|
||||||
# Download SVG
|
|
||||||
data = self.api.get_file(f"assets/{picture['picture']}")
|
|
||||||
with (self.config.p_data_pictures / f"{picture['id']}.svg").open("wb") as file:
|
|
||||||
file.write(data)
|
|
||||||
|
|
||||||
def download_includes(self):
|
|
||||||
includes = self.api.get("items/include", params={"limit": -1})
|
|
||||||
|
|
||||||
result = {}
|
|
||||||
for include in tqdm(includes, desc="Downloading includes"):
|
|
||||||
result[include["ident"]] = include["content"]
|
|
||||||
|
|
||||||
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 (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"):
|
|
||||||
contents.append(
|
|
||||||
{
|
|
||||||
"url_part": content["url_part"],
|
|
||||||
"content": content["content"],
|
|
||||||
"sidebar": content["sidebar"],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
with (self.config.p_data / "content.json").open("w", encoding="utf-8") as file:
|
|
||||||
json.dump(contents, file, ensure_ascii=False, indent=4)
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from enum import StrEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Edition(StrEnum):
|
||||||
|
n = "N"
|
||||||
|
e = "E"
|
||||||
|
a = "A"
|
||||||
|
ne = "NE"
|
||||||
|
ea = "EA"
|
||||||
|
nea = "NEA"
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
<div style="float: right;">
|
<div style="float: right;" data-quiz-shown>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
{% if chapter["video_url"] %}
|
{% if chapter["video_url"] %}
|
||||||
<a class="btn btn-secondary" href="{{ chapter["video_url"] }}" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"></path><path d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-6l-7 4V7z"></path></svg></a>
|
<a class="btn btn-secondary" href="{{ chapter["video_url"] }}" title="Video öffnen" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"></path><path d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-6l-7 4V7z"></path></svg></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-secondary" href="{{ edition }}_slide_{{ chapter["ident"] }}.html"><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M840-120v-640H120v320H40v-320q0-33 23.5-56.5T120-840h720q33 0 56.5 23.5T920-760v560q0 33-23.5 56.5T840-120ZM360-400q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm0-80q33 0 56.5-23.5T440-560q0-33-23.5-56.5T360-640q-33 0-56.5 23.5T280-560q0 33 23.5 56.5T360-480ZM40-80v-112q0-34 17.5-62.5T104-298q62-31 126-46.5T360-360q66 0 130 15.5T616-298q29 15 46.5 43.5T680-192v112H40Zm80-80h480v-32q0-11-5.5-20T580-226q-54-27-109-40.5T360-280q-56 0-111 13.5T140-226q-9 5-14.5 14t-5.5 20v32Zm240-400Zm0 400Z"/></svg></a>
|
<a class="btn btn-secondary" href="{{ edition }}_slide_{{ chapter["ident"] }}.html" title="Präsentationsfolien öffnen"><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M840-120v-640H120v320H40v-320q0-33 23.5-56.5T120-840h720q33 0 56.5 23.5T920-760v560q0 33-23.5 56.5T840-120ZM360-400q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm0-80q33 0 56.5-23.5T440-560q0-33-23.5-56.5T360-640q-33 0-56.5 23.5T280-560q0 33 23.5 56.5T360-480ZM40-80v-112q0-34 17.5-62.5T104-298q62-31 126-46.5T360-360q66 0 130 15.5T616-298q29 15 46.5 43.5T680-192v112H40Zm80-80h480v-32q0-11-5.5-20T580-226q-54-27-109-40.5T360-280q-56 0-111 13.5T140-226q-9 5-14.5 14t-5.5 20v32Zm240-400Zm0 400Z"/></svg></a>
|
||||||
|
<a class="btn btn-secondary" target="_blank" href="/hm" title="Hilfsmittel herunterladen"><svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 640 640" fill="#000000"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M192 112L304 112L304 200C304 239.8 336.2 272 376 272L464 272L464 512C464 520.8 456.8 528 448 528L192 528C183.2 528 176 520.8 176 512L176 128C176 119.2 183.2 112 192 112zM352 131.9L444.1 224L376 224C362.7 224 352 213.3 352 200L352 131.9zM192 64C156.7 64 128 92.7 128 128L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 250.5C512 233.5 505.3 217.2 493.3 205.2L370.7 82.7C358.7 70.7 342.5 64 325.5 64L192 64zM248 320C234.7 320 224 330.7 224 344C224 357.3 234.7 368 248 368L392 368C405.3 368 416 357.3 416 344C416 330.7 405.3 320 392 320L248 320zM248 416C234.7 416 224 426.7 224 440C224 453.3 234.7 464 248 464L392 464C405.3 464 416 453.3 416 440C416 426.7 405.3 416 392 416L248 416z"/></svg></a>
|
||||||
|
<button type="button" class="btn btn-secondary" id="quiz-mode-toggle" title="Nur Fragen anzeigen" aria-pressed="false"><svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 16 16" fill="#000000"><path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/><path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/><path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/></svg></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb" data-quiz-shown>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
||||||
<li class="breadcrumb-item"><a href="{{ edition }}_course_index.html">{{ name }}</a></li>
|
<li class="breadcrumb-item"><a href="{{ edition }}_course_index.html">{{ name }}</a></li>
|
||||||
@@ -15,7 +17,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<h1>{{ chapter["title"] }}</h1>
|
<h1 data-quiz-shown>{{ chapter["title"] }}</h1>
|
||||||
<p>{{ chapter["abstract"] }}</p>
|
<p>{{ chapter["abstract"] }}</p>
|
||||||
<div class="chapter-toc d-grid gap-2">
|
<div class="chapter-toc d-grid gap-2">
|
||||||
{% for section in chapter["sections"] %}
|
{% for section in chapter["sections"] %}
|
||||||
@@ -23,4 +25,4 @@
|
|||||||
<span class="btn-label">{{ number }}.{{loop.index}}</span>{{ section["title"]}}
|
<span class="btn-label">{{ number }}.{{loop.index}}</span>{{ section["title"]}}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -79,17 +79,17 @@
|
|||||||
<h5>Gesamtkurse</h5>
|
<h5>Gesamtkurse</h5>
|
||||||
<p>Diese Kurse richten sich an alle, die noch kein Amateurfunkzeugnis haben.</p>
|
<p>Diese Kurse richten sich an alle, die noch kein Amateurfunkzeugnis haben.</p>
|
||||||
<div class="row row-cols-lg-3 g-2">
|
<div class="row row-cols-lg-3 g-2">
|
||||||
<div class="col"><a href="N_course_index.html" class="btn btn-outline-dark w-100">N<br><small>Neueinsteiger</small></a></div>
|
<div class="col"><a href="N_course_index.html" class="btn w-100 text-white" style="background-color: #47ABE8; border-color: #47ABE8;">N<br><small>Neueinsteiger</small></a></div>
|
||||||
<div class="col"><a href="NE_course_index.html" class="btn btn-outline-dark w-100">N + E<br><small>Erweitert</small></a></div>
|
<div class="col"><a href="NE_course_index.html" class="btn w-100 text-white" style="background-color: #FE756C; border-color: #FE756C;">N + E<br><small>Erweitert</small></a></div>
|
||||||
<div class="col"><a href="NEA_course_index.html" class="btn btn-outline-dark w-100">N + E + A<br><small>Alles</small></a></div>
|
<div class="col"><a href="NEA_course_index.html" class="btn w-100 text-white" style="background-color: #3BB583; border-color: #3BB583;">N + E + A<br><small>Alles</small></a></div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<h5>Aufbaukurse</h5>
|
<h5>Aufbaukurse</h5>
|
||||||
<p>Für alle, die schon über ein Amateurfunkzeugnis der Klasse N oder E verfügen, gibt es folgende Aufbaukurse:</p>
|
<p>Für alle, die schon über ein Amateurfunkzeugnis der Klasse N oder E verfügen, gibt es folgende Aufbaukurse:</p>
|
||||||
<div class="row row-cols-lg-3 g-2">
|
<div class="row row-cols-lg-3 g-2">
|
||||||
<div class="col"><a href="E_course_index.html" class="btn btn-outline-dark w-100">N -> E</a></div>
|
<div class="col"><a href="E_course_index.html" class="btn w-100 text-white" style="background-color: #FE756C; border-color: #FE756C;">N -> E</a></div>
|
||||||
<div class="col"><a href="EA_course_index.html" class="btn btn-outline-dark w-100">N -> A</a></div>
|
<div class="col"><a href="EA_course_index.html" class="btn w-100 text-white" style="background-color: #3BB583; border-color: #3BB583;">N -> A</a></div>
|
||||||
<div class="col"><a href="A_course_index.html" class="btn btn-outline-dark w-100">E -> A</a></div>
|
<div class="col"><a href="A_course_index.html" class="btn w-100 text-white" style="background-color: #3BB583; border-color: #3BB583;">E -> A</a></div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<h5>Fragestunden</h5>
|
<h5>Fragestunden</h5>
|
||||||
@@ -118,10 +118,11 @@
|
|||||||
<div class="feature-icon bg-primary bg-gradient">
|
<div class="feature-icon bg-primary bg-gradient">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#5f6368"><path d="m380-300 280-180-280-180v360ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48" fill="#5f6368"><path d="m380-300 280-180-280-180v360ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<h2>Videokurs Klasse N</h2>
|
<h2>Videokurse</h2>
|
||||||
<p>In Zusammenarbeit mit DL2YMR ist ein Videokurs für die Klasse N entstanden, der auf seinem YouTube-Kanal abrufbar ist.</p>
|
<p>In Zusammenarbeit mit DL2YMR ist ein Videokurs für die Klasse N entstanden, der auf seinem YouTube-Kanal abrufbar ist.</p>
|
||||||
<div class="d-grid gap-2 d-md-block mb-5">
|
<div class="d-grid gap-2 d-md-block mb-5">
|
||||||
<a href="https://www.youtube.com/playlist?list=PLDpWnjHk5ERbcgpLPUaU0iTsD-wrmfENk" class="btn btn-primary">Zum Videokurs</a>
|
<a href="https://www.youtube.com/playlist?list=PLDpWnjHk5ERbcgpLPUaU0iTsD-wrmfENk" class="btn btn-primary">Zum Videokurs Klasse N</a>
|
||||||
|
<a href="https://www.youtube.com/playlist?list=PL_JZo16Mzs5XGeN5N81MwQ70dp7478VkM" class="btn btn-primary">Vorlesung Klasse N+E</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature col">
|
<div class="feature col">
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
||||||
<li class="breadcrumb-item"><a href="lernen.html">Lernen</a></li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">Kurse vor Ort</li>
|
<li class="breadcrumb-item active" aria-current="page">Kurse vor Ort</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -50,4 +49,4 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
||||||
<li class="breadcrumb-item"><a href="lernen.html">Lernen</a></li>
|
<li class="breadcrumb-item"><a href="index.html#lernen">Lernen</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">Kurse vor Ort</li>
|
<li class="breadcrumb-item active" aria-current="page">Kurse vor Ort</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<div>
|
<div data-quiz-shown>
|
||||||
Weiter zum nächsten Kapitel: <a href="{{ url }}">{{ title }}</a>
|
Weiter zum nächsten Kapitel: <a href="{{ url }}">{{ title }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<br/>
|
<br/>
|
||||||
<div>
|
<div data-quiz-shown>
|
||||||
Weiter zum nächsten Abschnitt: <a href="{{ url }}">{{ title }}</a>
|
Weiter zum nächsten Abschnitt: <a href="{{ url }}">{{ title }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<link rel="stylesheet" href="assets/katex/katex.min.css">
|
<link rel="stylesheet" href="assets/katex/katex.min.css">
|
||||||
<script defer src="assets/katex/katex.min.js"></script>
|
<script defer src="assets/katex/katex.min.js"></script>
|
||||||
<script defer src="assets/katex/auto-render.min.js"></script>
|
<script defer src="assets/katex/auto-render.min.js"></script>
|
||||||
|
<script defer src="assets/latex.js"></script>
|
||||||
<script defer src="assets/script.js"></script>
|
<script defer src="assets/script.js"></script>
|
||||||
<style>
|
<style>
|
||||||
</style>
|
</style>
|
||||||
@@ -65,6 +66,12 @@
|
|||||||
Lernen
|
Lernen
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="suche.html">
|
||||||
|
<span class="material-icons">search</span>
|
||||||
|
Suche
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="infos.html">
|
<a href="infos.html">
|
||||||
<span class="material-icons">info</span>
|
<span class="material-icons">info</span>
|
||||||
@@ -127,8 +134,8 @@
|
|||||||
<div class="d-flex flex-column align-items-center">
|
<div class="d-flex flex-column align-items-center">
|
||||||
<div class="d-flex gap-1 align-items-center mb-1">
|
<div class="d-flex gap-1 align-items-center mb-1">
|
||||||
Mit
|
Mit
|
||||||
<svg class="text-primary" xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="currentColor" viewBox="0 0 512 512"><path d="M47.6 300.4L228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6 0 115.2 0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/></svg></span>
|
<svg class="text-primary" xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" fill="#FE756C" viewBox="0 0 512 512"><path d="M47.6 300.4L228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6 0 115.2 0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/></svg></span>
|
||||||
vom <a class=link-secondary href="/infos#contributors">50ohm-Team</a> entwickelt
|
vom <a class=link-secondary href="/infos.html#contributors">50ohm-Team</a> entwickelt
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-1">Ein Angebot des Deutschen Amateur-Radio-Clubs e. V.</div>
|
<div class="mb-1">Ein Angebot des Deutschen Amateur-Radio-Clubs e. V.</div>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
||||||
<li class="breadcrumb-item"><a href="lernen.html">Lernen</a></li>
|
<li class="breadcrumb-item"><a href="index.html#lernen">Lernen</a></li>
|
||||||
<li class="breadcrumb-item active" aria-current="page">Ausbildungspaten</li>
|
<li class="breadcrumb-item active" aria-current="page">Ausbildungspaten</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,25 +1,37 @@
|
|||||||
<div class="question layout-{{ layout }}" id="{{ number }}">
|
<div class="question layout-{{ layout }}" id="{{ number }}" data-quiz-shown>
|
||||||
<div class="question-text">{{ number }}: {{ question }}</div>
|
<div class="question-text">
|
||||||
{% if picture_question %}
|
{{ number }}: {{ question }}
|
||||||
<div class="question-picture"><img src="pictures/{{ picture_question }}.svg" alt="{{ alt_text_question }}"></div>
|
|
||||||
{% endif %}
|
|
||||||
<div class="answers">
|
|
||||||
{% for i in range(4) %}
|
|
||||||
<button onclick="answer(this, {{ 'true' if i == 0 else 'false' }} );">
|
|
||||||
{% if answers[i] %}
|
|
||||||
{{ answers[i] }}
|
|
||||||
{% endif %}
|
|
||||||
{% if answer_pictures[i] %}
|
|
||||||
<div class="question-picture"><img src="pictures/{{ answer_pictures[i] }}.svg" alt="{{ alt_text_answers[i] }}"></div>
|
|
||||||
{% endif %}
|
|
||||||
{% if i == 0 %}
|
|
||||||
<div class="result material-icons">check</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="result material-icons">clear</div>
|
|
||||||
{% endif %}
|
|
||||||
</button>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if picture_question %}
|
||||||
|
<div class="question-picture"><img src="pictures/{{ picture_question }}.svg" alt="{{ alt_text_question }}"></div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="answers">
|
||||||
|
{% for i in range(4) %}
|
||||||
|
<button onclick="answer(this, {{ 'true' if i == 0 else 'false' }} );">
|
||||||
|
{% if answers[i] %}
|
||||||
|
{{ answers[i] }}
|
||||||
|
{% endif %}
|
||||||
|
{% if answer_pictures[i] %}
|
||||||
|
<div class="question-picture"><img src="pictures/{{ answer_pictures[i] }}.svg" alt="{{ alt_text_answers[i] }}"></div>
|
||||||
|
{% endif %}
|
||||||
|
{% if i == 0 %}
|
||||||
|
<div class="result material-icons">check</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="result material-icons">clear</div>
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="solution" data-quiz-shown>
|
||||||
|
{% if has_solution %}
|
||||||
|
<a class="btn btn-warning btn-sm d-inline-flex align-items-center" href="{{ number }}.html" aria-label="Lösungshinweis zu Frage {{ number }}" title="Lösungshinweis">
|
||||||
|
<svg class="solution-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="currentColor" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M420.9 448C428.2 425.7 442.8 405.5 459.3 388.1C492 353.7 512 307.2 512 256C512 150 426 64 320 64C214 64 128 150 128 256C128 307.2 148 353.7 180.7 388.1C197.2 405.5 211.9 425.7 219.1 448L420.8 448zM416 496L224 496L224 512C224 556.2 259.8 592 304 592L336 592C380.2 592 416 556.2 416 512L416 496zM312 176C272.2 176 240 208.2 240 248C240 261.3 229.3 272 216 272C202.7 272 192 261.3 192 248C192 181.7 245.7 128 312 128C325.3 128 336 138.7 336 152C336 165.3 325.3 176 312 176z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Lösungsweg anzeigen</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
<div style="float: right;">
|
<div style="float: right;" data-quiz-shown>
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
{% if section["video_url"] %}
|
{% if section["video_url"] %}
|
||||||
<a class="btn btn-secondary" href="{{ section["video_url"] }}" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"></path><path d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-6l-7 4V7z"></path></svg></a>
|
<a class="btn btn-secondary" href="{{ section["video_url"] }}" title="Video öffnen" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"></path><path d="M21 3H3c-1.11 0-2 .89-2 2v12c0 1.1.89 2 2 2h5v2h8v-2h5c1.1 0 1.99-.9 1.99-2L23 5c0-1.11-.9-2-2-2zm0 14H3V5h18v12zm-5-6l-7 4V7z"></path></svg></a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="btn btn-secondary" href="{{ edition }}_slide_{{ chapter["ident"] }}.html#{{ section_id }}"><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M840-120v-640H120v320H40v-320q0-33 23.5-56.5T120-840h720q33 0 56.5 23.5T920-760v560q0 33-23.5 56.5T840-120ZM360-400q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm0-80q33 0 56.5-23.5T440-560q0-33-23.5-56.5T360-640q-33 0-56.5 23.5T280-560q0 33 23.5 56.5T360-480ZM40-80v-112q0-34 17.5-62.5T104-298q62-31 126-46.5T360-360q66 0 130 15.5T616-298q29 15 46.5 43.5T680-192v112H40Zm80-80h480v-32q0-11-5.5-20T580-226q-54-27-109-40.5T360-280q-56 0-111 13.5T140-226q-9 5-14.5 14t-5.5 20v32Zm240-400Zm0 400Z"/></svg></a>
|
<a class="btn btn-secondary" href="{{ edition }}_slide_{{ chapter["ident"] }}.html#{{ section_id }}" title="Präsentationsfolien öffnen"><svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M840-120v-640H120v320H40v-320q0-33 23.5-56.5T120-840h720q33 0 56.5 23.5T920-760v560q0 33-23.5 56.5T840-120ZM360-400q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm0-80q33 0 56.5-23.5T440-560q0-33-23.5-56.5T360-640q-33 0-56.5 23.5T280-560q0 33 23.5 56.5T360-480ZM40-80v-112q0-34 17.5-62.5T104-298q62-31 126-46.5T360-360q66 0 130 15.5T616-298q29 15 46.5 43.5T680-192v112H40Zm80-80h480v-32q0-11-5.5-20T580-226q-54-27-109-40.5T360-280q-56 0-111 13.5T140-226q-9 5-14.5 14t-5.5 20v32Zm240-400Zm0 400Z"/></svg></a>
|
||||||
|
<a class="btn btn-secondary" target="_blank" href="/hm" title="Hilfsmittel herunterladen"><svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 640 640" fill="#000000"><!--!Font Awesome Free v7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M192 112L304 112L304 200C304 239.8 336.2 272 376 272L464 272L464 512C464 520.8 456.8 528 448 528L192 528C183.2 528 176 520.8 176 512L176 128C176 119.2 183.2 112 192 112zM352 131.9L444.1 224L376 224C362.7 224 352 213.3 352 200L352 131.9zM192 64C156.7 64 128 92.7 128 128L128 512C128 547.3 156.7 576 192 576L448 576C483.3 576 512 547.3 512 512L512 250.5C512 233.5 505.3 217.2 493.3 205.2L370.7 82.7C358.7 70.7 342.5 64 325.5 64L192 64zM248 320C234.7 320 224 330.7 224 344C224 357.3 234.7 368 248 368L392 368C405.3 368 416 357.3 416 344C416 330.7 405.3 320 392 320L248 320zM248 416C234.7 416 224 426.7 224 440C224 453.3 234.7 464 248 464L392 464C405.3 464 416 453.3 416 440C416 426.7 405.3 416 392 416L248 416z"/></svg></a>
|
||||||
|
<button type="button" class="btn btn-secondary" id="quiz-mode-toggle" title="Nur Fragen anzeigen" aria-pressed="false"><svg xmlns="http://www.w3.org/2000/svg" height="24" width="24" viewBox="0 0 16 16" fill="#000000"><path d="M13.359 11.238C15.06 9.72 16 8 16 8s-3-5.5-8-5.5a7.028 7.028 0 0 0-2.79.588l.77.771A5.944 5.944 0 0 1 8 3.5c2.12 0 3.879 1.168 5.168 2.457A13.134 13.134 0 0 1 14.828 8c-.058.087-.122.183-.195.288-.335.48-.83 1.12-1.465 1.755-.165.165-.337.328-.517.486l.708.709z"/><path d="M11.297 9.176a3.5 3.5 0 0 0-4.474-4.474l.823.823a2.5 2.5 0 0 1 2.829 2.829l.822.822zm-2.943 1.299.822.822a3.5 3.5 0 0 1-4.474-4.474l.823.823a2.5 2.5 0 0 0 2.829 2.829z"/><path d="M3.35 5.47c-.18.16-.353.322-.518.487A13.134 13.134 0 0 0 1.172 8l.195.288c.335.48.83 1.12 1.465 1.755C4.121 11.332 5.881 12.5 8 12.5c.716 0 1.39-.133 2.02-.36l.77.772A7.029 7.029 0 0 1 8 13.5C3 13.5 0 8 0 8s.939-1.721 2.641-3.238l.708.709zm10.296 8.884-12-12 .708-.708 12 12-.708.708z"/></svg></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav aria-label="breadcrumb">
|
<nav aria-label="breadcrumb" data-quiz-shown>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
<li class="breadcrumb-item"><a href="index.html">Home</a></li>
|
||||||
<li class="breadcrumb-item"><a href="{{ edition }}_course_index.html">{{ name }}</a></li>
|
<li class="breadcrumb-item"><a href="{{ edition }}_course_index.html">{{ name }}</a></li>
|
||||||
@@ -16,7 +18,7 @@
|
|||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<h1>{{ section["title"] }}</h1>
|
<h1 data-quiz-shown>{{ section["title"] }}</h1>
|
||||||
|
|
||||||
<a id="margin_0"></a>
|
<a id="margin_0"></a>
|
||||||
{{ section["content"] }}
|
{{ section["content"] }}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<div class="card" role="alert">
|
||||||
|
<h2 class="card-header text-bg-warning bg-opacity-50">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="currentColor" aria-hidden="true" focusable="false" style="width: 1.1em; height: 1.1em; vertical-align: -0.1em;">
|
||||||
|
<path d="M420.9 448C428.2 425.7 442.8 405.5 459.3 388.1C492 353.7 512 307.2 512 256C512 150 426 64 320 64C214 64 128 150 128 256C128 307.2 148 353.7 180.7 388.1C197.2 405.5 211.9 425.7 219.1 448L420.8 448zM416 496L224 496L224 512C224 556.2 259.8 592 304 592L336 592C380.2 592 416 556.2 416 512L416 496zM312 176C272.2 176 240 208.2 240 248C240 261.3 229.3 272 216 272C202.7 272 192 261.3 192 248C192 181.7 245.7 128 312 128C325.3 128 336 138.7 336 152C336 165.3 325.3 176 312 176z"/>
|
||||||
|
</svg>
|
||||||
|
Lösungsweg: {{number}}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="card-body text-bg-light">
|
||||||
|
{{question}}
|
||||||
|
</div>
|
||||||
|
<hr class="m-0" />
|
||||||
|
<div class="card-body">
|
||||||
|
{{solution}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<div class="question layout-{{ layout }} for-solution" id="{{ number }}">
|
||||||
|
<div class="question-text m-0">Frage: {{ question }}</div>
|
||||||
|
{% if picture_question %}
|
||||||
|
<div class="question-picture"><img src="pictures/{{ picture_question }}.svg" alt="{{ alt_text_question }}"></div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="answers">
|
||||||
|
{% for i in range(1) %}
|
||||||
|
<div>
|
||||||
|
<b>{{ 'Lösung:' if i == 0 else 'Falsch:' }}</b>
|
||||||
|
{% if answers[i] %}
|
||||||
|
{{ answers[i] }}
|
||||||
|
{% endif %}
|
||||||
|
{% if answer_pictures[i] %}
|
||||||
|
<div class="question-picture"><img src="pictures/{{ answer_pictures[i] }}.svg" alt="{{ alt_text_answers[i] }}"></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
+11
-10
@@ -6,7 +6,8 @@
|
|||||||
<section style="font-size: smaller;">
|
<section style="font-size: smaller;">
|
||||||
<h4>Navigation</h4>
|
<h4>Navigation</h4>
|
||||||
<div id="left">
|
<div id="left">
|
||||||
<p>Zwischen den Folien und Abschnitten kann man mittels der Pfeiltasten hin- und herspringen, dazu kann man auch
|
<p>Zwischen den Folien und Abschnitten lässt sich mittels der Pfeiltasten hin- und herspringen, dazu lassen sich
|
||||||
|
auch
|
||||||
die Pfeiltasten am Computer nutzen.</p>
|
die Pfeiltasten am Computer nutzen.</p>
|
||||||
<img src="assets/images/slides/navigation.png" alt="Navigationspfeile für die Präsentation" />
|
<img src="assets/images/slides/navigation.png" alt="Navigationspfeile für die Präsentation" />
|
||||||
</div>
|
</div>
|
||||||
@@ -44,8 +45,8 @@
|
|||||||
<div id="left">
|
<div id="left">
|
||||||
<p>Die Präsentation ist zweidimensional aufgebaut. Dadurch sind in Spalten die einzelnen Abschnitte eines
|
<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>
|
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
|
<p>Tippt man ein „o“ ein, bekommt man eine Übersicht über alle Folien des Foliensatzes. Das hilft,
|
||||||
sich zunächst einen Überblick zu verschaffen oder sich zu orientieren, wenn man das Gefühlt hat sich
|
sich zunächst einen Überblick zu verschaffen oder sich zu orientieren, wenn man das Gefüht hat, sich
|
||||||
„verlaufen“ zu haben. Die Navigation erfolgt über die Pfeiltasten.</p>
|
„verlaufen“ zu haben. Die Navigation erfolgt über die Pfeiltasten.</p>
|
||||||
</div>
|
</div>
|
||||||
<div id="right">
|
<div id="right">
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
<li>Wenn man mit einem Projektor arbeitet, stellt man im Betriebssystem die Nutzung von 2 Monitoren ein: Die
|
<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
|
Referentenansicht wird dann zum Beispiel auf dem Laptop angezeigt, während die Teilnehmer die
|
||||||
Präsentation angezeigt bekommen.</li>
|
Präsentation angezeigt bekommen.</li>
|
||||||
<li>Bei einer Online-Präsentation, wie beispielsweise auf <a href="https://treff.darc.de/">TREFF.darc.de</a>
|
<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>
|
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
|
<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>
|
werden, um im neuen Kapitel eine neue Referentenansicht zu öffnen.</li>
|
||||||
@@ -87,26 +88,26 @@
|
|||||||
</section>
|
</section>
|
||||||
<section style="font-size: smaller;">
|
<section style="font-size: smaller;">
|
||||||
<h4>Vollbild</h4>
|
<h4>Vollbild</h4>
|
||||||
<p>Tippt man ein „f“ ein, wird die aktuelle Folie im Vollbild angezeigt. Mit „Esc“ kann man diesen wieder
|
<p>Tippt man ein „f“ ein, wird die aktuelle Folie im Vollbild angezeigt. Mit „Esc“ kann man das Vollbild wieder
|
||||||
verlassen.</p>
|
verlassen.</p>
|
||||||
<p>Das ist insbesondere für den Bildschirm mit der Präsentation für das Publikum praktisch.</li>
|
<p>Das ist insbesondere für den Bildschirm mit der Präsentation für das Publikum praktisch.</li>
|
||||||
</section>
|
</section>
|
||||||
<section style="font-size: smaller;">
|
<section style="font-size: smaller;">
|
||||||
<h4>Ausblenden</h4>
|
<h4>Ausblenden</h4>
|
||||||
<p>Tippt man ein „b“ ein, wird die Präsentation ausgeblendet.</p>
|
<p>Tippt man ein „b“ ein, wird die Präsentation ausgeblendet.</p>
|
||||||
<p>Sie kann wie folgte wieder eingeblendet werden:</p>
|
<p>Sie kann wie folgt wieder eingeblendet werden:</p>
|
||||||
<ul class="list">
|
<ul class="list">
|
||||||
<li>Durch klicken in das Fenster.</li>
|
<li>Durch Klicken in das Fenster.</li>
|
||||||
<li>Durch nochmaliges Drücken von „b“.</li>
|
<li>Durch nochmaliges Drücken von „b“.</li>
|
||||||
<li>Durch klicken der Schaltfläche „Resume presentation:</li>
|
<li>Durch Klicken der Schaltfläche „Resume presentation“.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<img src="assets/images/slides/resume.png" alt="Schaltfläche für Resume Presentation" />
|
<img src="assets/images/slides/resume.png" alt="Schaltfläche für Resume Presentation" />
|
||||||
</section>
|
</section>
|
||||||
<section style="font-size: smaller;">
|
<section style="font-size: smaller;">
|
||||||
<h4>Zoom</h4>
|
<h4>Zoom</h4>
|
||||||
<p>Bei gedrückter Alt-Taste und einem Mausklick in der Präsentation wird in diesen Teil hineingezoomt. Das ist
|
<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
|
praktisch, um Details von Schaltungen hervorzuheben. Durch einen nochmaligen Mausklick zusammen mit Alt wird
|
||||||
wieder herausgezoomt.</p>
|
wieder herausgezoomt.</p>
|
||||||
<p>Das Zoomen funktioniert nur im ausgewählten Fenster. Die Referentenansicht ist hier nicht mit dem
|
<p>Das Zoomen funktioniert nur im ausgewählten Fenster. Die Referentenansicht ist hier nicht mit der
|
||||||
Präsenationsansicht gesynct.</p>
|
Präsenationsansicht gesynct.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -17,6 +17,11 @@
|
|||||||
<!-- Chart plugin -->
|
<!-- Chart plugin -->
|
||||||
<script src="assets/reveal.js-plugins/chart/plugin.js"></script>
|
<script src="assets/reveal.js-plugins/chart/plugin.js"></script>
|
||||||
<script src="assets/chartjs/4.4.1/chart.min.js"></script>
|
<script src="assets/chartjs/4.4.1/chart.min.js"></script>
|
||||||
|
<!-- KaTeX for formulas -->
|
||||||
|
<link rel="stylesheet" href="assets/katex/katex.min.css">
|
||||||
|
<script defer src="assets/katex/katex.min.js"></script>
|
||||||
|
<script defer src="assets/katex/auto-render.min.js"></script>
|
||||||
|
<script defer src="assets/latex.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -49,7 +54,7 @@
|
|||||||
pdfSeparateFragments: false,
|
pdfSeparateFragments: false,
|
||||||
|
|
||||||
// Learn about plugins: https://revealjs.com/plugins/
|
// Learn about plugins: https://revealjs.com/plugins/
|
||||||
plugins: [ RevealZoom, RevealNotes, RevealSearch, RevealMarkdown, RevealHighlight, RevealMath.KaTeX, RevealChart ]
|
plugins: [ RevealZoom, RevealNotes, RevealSearch, RevealMarkdown, RevealHighlight, RevealChart ]
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ Wenn man allerdings nur den Widerstand und die Spannung kennt und den entspreche
|
|||||||
|
|
||||||
$ I = \dfrac{U}{R} $
|
$ I = \dfrac{U}{R} $
|
||||||
|
|
||||||
[question:64]
|
[question:NB504]
|
||||||
|
|
||||||
Würde man die 10 V-Batterie in dem Beispiel durch eine 20 V-Batterie ersetzen, dann würde sich auch der Strom von 1 mA auf 2 mA erhöhen. Wenn man also die Spannung verdoppelt, dann verdoppelt sich auch der Strom. Entsprechend würde sich auch der Strom auf 0,5 mA halbieren, wenn man die Spannung auf 5 V halbieren würde.
|
Würde man die 10 V-Batterie in dem Beispiel durch eine 20 V-Batterie ersetzen, dann würde sich auch der Strom von 1 mA auf 2 mA erhöhen. Wenn man also die Spannung verdoppelt, dann verdoppelt sich auch der Strom. Entsprechend würde sich auch der Strom auf 0,5 mA halbieren, wenn man die Spannung auf 5 V halbieren würde.
|
||||||
|
|
||||||
@@ -73,8 +73,8 @@ CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[question:397]
|
[question:BB102]
|
||||||
[question:515]
|
[question:BE105]
|
||||||
|
|
||||||
<indepth>
|
<indepth>
|
||||||
*QSO* ist eine Abkürzung, die ursprünglich aus der Morsetelegrafie stammt und "ich habe direkten Kontakt zu Station X" bedeutet. Daran angelehnt, sprechen Funkamateure oftmals von einem QSO, wenn sie eine erfolgreiche Funkverbindung meinen.
|
*QSO* ist eine Abkürzung, die ursprünglich aus der Morsetelegrafie stammt und "ich habe direkten Kontakt zu Station X" bedeutet. Daran angelehnt, sprechen Funkamateure oftmals von einem QSO, wenn sie eine erfolgreiche Funkverbindung meinen.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ CQ CQ hier ist DL1PZ mit einem allgemeinen Anruf, hier ist DL1PZ und hört.
|
|||||||
[photo:123:n_rst_s-meter:Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt]
|
[photo:123:n_rst_s-meter:Display eines IC9700-Transceivers, hervorgehoben ist das S-Meter, das den aktuellen Empfangspegel anzeigt]
|
||||||
|
|
||||||
---
|
---
|
||||||
[question:2482]
|
[question:NB201]
|
||||||
---
|
---
|
||||||
|
|
||||||
## Slide Tabelle
|
## Slide Tabelle
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from test.util import paragraph
|
|||||||
def test_dash_html():
|
def test_dash_html():
|
||||||
assertions = {
|
assertions = {
|
||||||
"Foo - Bar": "Foo – Bar",
|
"Foo - Bar": "Foo – Bar",
|
||||||
|
"Foo-Bar": "Foo-Bar",
|
||||||
}
|
}
|
||||||
|
|
||||||
for assertion in assertions:
|
for assertion in assertions:
|
||||||
|
|||||||
+10
-10
@@ -4,11 +4,14 @@ import pytest
|
|||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from mistletoe import Document
|
from mistletoe import Document
|
||||||
|
|
||||||
|
import src.config as config
|
||||||
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||||
|
|
||||||
|
conf = config.Config()
|
||||||
|
|
||||||
|
|
||||||
def parse_katalog():
|
def parse_katalog():
|
||||||
with open("data/fragenkatalog3b.json") as fragenkatalog_file:
|
with open(conf.p_data_fragenkatalog) as fragenkatalog_file:
|
||||||
fragenkatalog = json.load(fragenkatalog_file)
|
fragenkatalog = json.load(fragenkatalog_file)
|
||||||
|
|
||||||
questions = {}
|
questions = {}
|
||||||
@@ -28,17 +31,15 @@ def parse_katalog():
|
|||||||
|
|
||||||
env = Environment(loader=FileSystemLoader("templates/html"))
|
env = Environment(loader=FileSystemLoader("templates/html"))
|
||||||
question_template = env.get_template("question.html")
|
question_template = env.get_template("question.html")
|
||||||
questions = parse_katalog()
|
|
||||||
|
|
||||||
|
|
||||||
def question_stub(input):
|
def question_stub(number):
|
||||||
|
questions = parse_katalog()
|
||||||
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
||||||
with open("data/metadata3b.json") as metadata_file:
|
with open(conf.p_data_metadata) as metadata_file:
|
||||||
metadata = json.load(metadata_file)
|
metadata = json.load(metadata_file)
|
||||||
number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123
|
|
||||||
|
|
||||||
question = questions[number]
|
question = questions[number]
|
||||||
metadata = metadata[f"{input}"]
|
metadata = metadata[f"{number}"]
|
||||||
|
|
||||||
if "answer_a" in question:
|
if "answer_a" in question:
|
||||||
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
|
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
|
||||||
@@ -71,9 +72,8 @@ def question_stub(input):
|
|||||||
|
|
||||||
|
|
||||||
def include_stub(include):
|
def include_stub(include):
|
||||||
with open("data/includes.json") as file:
|
with open(conf.p_data_html / f"{include}.html") as file:
|
||||||
includes = json.load(file)
|
return file.read()
|
||||||
return includes.get(include)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip("Requires special files")
|
@pytest.mark.skip("Requires special files")
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import pytest
|
|||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
from mistletoe import Document
|
from mistletoe import Document
|
||||||
|
|
||||||
|
import src.config as config
|
||||||
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
from renderer.fifty_ohm_html_slide_renderer import FiftyOhmHtmlSlideRenderer
|
||||||
|
|
||||||
|
conf = config.Config()
|
||||||
|
|
||||||
|
|
||||||
def parse_katalog():
|
def parse_katalog():
|
||||||
with open("data/fragenkatalog3b.json") as fragenkatalog_file:
|
with open(conf.p_data_fragenkatalog) as fragenkatalog_file:
|
||||||
fragenkatalog = json.load(fragenkatalog_file)
|
fragenkatalog = json.load(fragenkatalog_file)
|
||||||
|
|
||||||
questions = {}
|
questions = {}
|
||||||
@@ -43,14 +46,13 @@ def filter_shuffle_answers(seq):
|
|||||||
return seq
|
return seq
|
||||||
|
|
||||||
|
|
||||||
def question_stub(input):
|
def question_stub(number):
|
||||||
|
questions = parse_katalog()
|
||||||
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
"""Combines the original question dataset from BNetzA with our internal metadata"""
|
||||||
with open("data/metadata3b.json") as metadata_file:
|
with open(conf.p_data_metadata) as metadata_file:
|
||||||
metadata = json.load(metadata_file)
|
metadata = json.load(metadata_file)
|
||||||
number = metadata[f"{input}"]["number"] # Fragennummer z.B. AB123
|
|
||||||
|
|
||||||
question = questions[number]
|
question = questions[number]
|
||||||
metadata = metadata[f"{input}"]
|
metadata = metadata[f"{number}"]
|
||||||
|
|
||||||
if "answer_a" in question:
|
if "answer_a" in question:
|
||||||
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
|
answers = [question["answer_a"], question["answer_b"], question["answer_c"], question["answer_d"]]
|
||||||
@@ -86,7 +88,6 @@ env = Environment(loader=FileSystemLoader("templates/"))
|
|||||||
env.filters["shuffle_answers"] = filter_shuffle_answers
|
env.filters["shuffle_answers"] = filter_shuffle_answers
|
||||||
question_template = env.get_template("slide/question.html")
|
question_template = env.get_template("slide/question.html")
|
||||||
slide_template = env.get_template("slide/slide.html")
|
slide_template = env.get_template("slide/slide.html")
|
||||||
questions = parse_katalog()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip("Requires special files")
|
@pytest.mark.skip("Requires special files")
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import mistletoe
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from renderer.fifty_ohm_html_renderer import FiftyOhmHtmlRenderer
|
||||||
|
from renderer.fifty_ohm_latex_renderer import FiftyOhmLaTeXRenderer
|
||||||
|
from test.util import paragraph
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.html
|
||||||
|
def test_index_html():
|
||||||
|
assertions = {
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
|
+ '<span id="index_antenne__yagi_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
|
+ '<span id="index_antenne__yagi_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
|
+ '<span id="index_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
|
+ '<span id="index_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Test Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb werden Yagi-Antennen"
|
||||||
|
+ '<span id="index_test_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " oft gedreht"
|
||||||
|
),
|
||||||
|
"Im Betrieb [index:Antenne] und nochmals [index:Antenne] oft gedreht": (
|
||||||
|
"Im Betrieb"
|
||||||
|
+ '<span id="index_antenne" class="index-anchor" aria-hidden="true"></span>'
|
||||||
|
+ " und nochmals oft gedreht"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in assertions.items():
|
||||||
|
assert mistletoe.markdown(key, FiftyOhmHtmlRenderer) == paragraph(value)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.latex
|
||||||
|
def test_index_latex():
|
||||||
|
assertions = {
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne:Yagi-Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne!Yagi-Antenne} oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen[index:Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Antenne} oft gedreht", # noqa: E501
|
||||||
|
"Im Betrieb werden Yagi-Antennen [index:Test Antenne] oft gedreht": "Im Betrieb werden Yagi-Antennen\\index{Test Antenne} oft gedreht", # noqa: E501
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in assertions.items():
|
||||||
|
assert mistletoe.markdown(key, FiftyOhmLaTeXRenderer) == "\n" + value + "\n"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 2
|
revision = 3
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8,10 +8,11 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "joblib" },
|
|
||||||
{ name = "mistletoe" },
|
{ name = "mistletoe" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
|
{ name = "rich" },
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
|
{ name = "typer" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
@@ -25,17 +26,18 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "jinja2", specifier = "==3.1.6" },
|
{ name = "jinja2", specifier = "==3.1.6" },
|
||||||
{ name = "joblib", specifier = "==1.5.1" },
|
|
||||||
{ name = "mistletoe", specifier = "==1.4.0" },
|
{ name = "mistletoe", specifier = "==1.4.0" },
|
||||||
{ name = "requests", specifier = "==2.32.4" },
|
{ name = "requests", specifier = "==2.32.4" },
|
||||||
|
{ name = "rich", specifier = ">=14.2.0" },
|
||||||
{ name = "tqdm", specifier = "==4.67.1" },
|
{ name = "tqdm", specifier = "==4.67.1" },
|
||||||
|
{ name = "typer", specifier = ">=0.21.1" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "pytest", specifier = "==8.4.1" },
|
{ name = "pytest", specifier = "==8.4.1" },
|
||||||
{ name = "pytest-cov", specifier = "==6.2.1" },
|
{ name = "pytest-cov", specifier = "==6.2.1" },
|
||||||
{ name = "ruff", specifier = "==0.12.2" },
|
{ name = "ruff", specifier = "==0.15.12" },
|
||||||
{ name = "ty", specifier = "==0.0.1a13" },
|
{ name = "ty", specifier = "==0.0.1a13" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -83,6 +85,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -165,12 +179,15 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "joblib"
|
name = "markdown-it-py"
|
||||||
version = "1.5.1"
|
version = "4.0.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/fe/0f5a938c54105553436dbff7a61dc4fed4b1b2c98852f8833beaf4d5968f/joblib-1.5.1.tar.gz", hash = "sha256:f4f86e351f39fe3d0d32a9f2c3d8af1ee4cec285aafcb27003dda5205576b444", size = 330475, upload-time = "2025-05-23T12:04:37.097Z" }
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746, upload-time = "2025-05-23T12:04:35.124Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -211,6 +228,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mistletoe"
|
name = "mistletoe"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -293,28 +319,50 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "rich"
|
||||||
version = "0.12.2"
|
version = "14.2.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
|
{ url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
|
]
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
|
[[package]]
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
|
name = "ruff"
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
|
version = "0.15.12"
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
|
source = { registry = "https://pypi.org/simple" }
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
|
sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" }
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
|
{ url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
|
{ url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
|
{ url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
|
{ url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
|
{ url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellingham"
|
||||||
|
version = "1.5.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -354,6 +402,30 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c7/62/fe6cc92db2aa1a344937e5313ba3bbdaed24e54332b8a12fe28a92d314ce/ty-0.0.1a13-py3-none-win_arm64.whl", hash = "sha256:5f2237bb301078d51d006170992abb1e5f3b37cec47a11e6db416a76346f60b5", size = 6727050, upload-time = "2025-07-02T17:52:12.874Z" },
|
{ url = "https://files.pythonhosted.org/packages/c7/62/fe6cc92db2aa1a344937e5313ba3bbdaed24e54332b8a12fe28a92d314ce/ty-0.0.1a13-py3-none-win_arm64.whl", hash = "sha256:5f2237bb301078d51d006170992abb1e5f3b37cec47a11e6db416a76346f60b5", size = 6727050, upload-time = "2025-07-02T17:52:12.874Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typer"
|
||||||
|
version = "0.21.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "rich" },
|
||||||
|
{ name = "shellingham" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user